From 14ce8416be47af1cc8437c83f7e2b40800aca985 Mon Sep 17 00:00:00 2001
From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com>
Date: Mon, 12 Apr 2021 21:26:31 +0200
Subject: [PATCH] Introducing metrics insights! (#228)
---
source/app/web/instance.mjs | 60 +++-
source/app/web/statics/about/index.html | 316 +++++++++++++++++++++
source/app/web/statics/about/script.js | 113 ++++++++
source/app/web/statics/about/style.css | 360 ++++++++++++++++++++++++
source/app/web/statics/app.js | 1 +
source/app/web/statics/index.html | 16 +-
source/app/web/statics/style.css | 5 +
source/plugins/achievements/index.mjs | 10 +-
source/plugins/languages/README.md | 1 +
source/plugins/languages/index.mjs | 8 +-
source/plugins/languages/metadata.yml | 10 +
11 files changed, 886 insertions(+), 14 deletions(-)
create mode 100644 source/app/web/statics/about/index.html
create mode 100644 source/app/web/statics/about/script.js
create mode 100644 source/app/web/statics/about/style.css
diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs
index a033a6d1..6e52eff6 100644
--- a/source/app/web/instance.mjs
+++ b/source/app/web/instance.mjs
@@ -89,7 +89,14 @@
let requests = {limit:0, used:0, remaining:0, reset:NaN}
if (!conf.settings.notoken) {
requests = (await rest.rateLimit.get()).data.rate
- setInterval(async() => requests = (await rest.rateLimit.get()).data.rate, 5*60*1000)
+ setInterval(async() => {
+ try {
+ requests = (await rest.rateLimit.get()).data.rate
+ }
+ catch {
+ console.debug("metrics/app > failed to update remaining requests")
+ }
+ }, 5*60*1000)
}
//Web
app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
@@ -144,6 +151,57 @@
}
})
+ //About routes
+ app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
+ app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
+ app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
+ app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
+ app.get("/about/query/:login/", ...middlewares, async(req, res) => {
+ //Check username
+ const login = req.params.login?.replace(/[\n\r]/g, "")
+ if (!/^[-\w]+$/i.test(login)) {
+ console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
+ return res.status(400).send("Bad request: username seems invalid")
+ }
+ //Compute metrics
+ try {
+ //Read cached data if possible
+ if ((!debug)&&(cached)&&(cache.get(`about.${login}`))) {
+ console.debug(`metrics/app/${login}/insights > using cached results`)
+ return res.send(cache.get(`about.${login}`))
+ }
+ //Compute metrics
+ console.debug(`metrics/app/${login}/insights > compute insights`)
+ const json = await metrics({
+ login, q:{
+ template:"classic",
+ achievements:true, "achievements.threshold":"X",
+ isocalendar:true, "isocalendar.duration":"full-year",
+ languages:true, "languages.limit":0,
+ activity:true, "activity.limit":100, "activity.days":0,
+ notable:true,
+ },
+ }, {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true}, notable:{enabled:true}}, conf, convert:"json"}, {Plugins, Templates})
+ //Cache
+ if ((!debug)&&(cached)) {
+ const maxage = Math.round(Number(req.query.cache))
+ cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
+ }
+ return res.json(json)
+ }
+ //Internal error
+ catch (error) {
+ //Not found user
+ if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
+ console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
+ return res.status(404).send("Not found: unknown user or organization")
+ }
+ //General error
+ console.error(error)
+ return res.status(500).send("Internal Server Error: failed to process metrics correctly")
+ }
+ })
+
//Metrics
const pending = new Map()
app.get("/:login/:repository?", ...middlewares, async(req, res) => {
diff --git a/source/app/web/statics/about/index.html b/source/app/web/statics/about/index.html
new file mode 100644
index 00000000..03b85d00
--- /dev/null
+++ b/source/app/web/statics/about/index.html
@@ -0,0 +1,316 @@
+
+
+
+ Metrics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search a GitHub user
+
+
{{ requests.remaining }} GitHub requests remaining
+
+
+
+
+ {{ pending ? 'Working on it :)' : 'Search user!' }}
+
+
+
+ Display rankings, highlights, commits calendar, used languages and recent activity from any user account!
+
+
+ Share this profile using {{ url }}
+
+
+
+
+ Generating insights for {{ user }}...
+
+
+
+ An error occurred while generating metrics :(
+ {{ error.message }}
+
+
+
+
+
+
+
+
+
+
{{ account.name }}
+
{{ account.login }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ format("number", leaderboard.user) }}{{ {1:"st", 2:"nd", 3:"rd"}[leaderboard.user%10] ?? "th" }}
+
/ {{ format("number", leaderboard.total, {notation:"compact", compactDisplay:"long"}) }} {{ leaderboard.type }}
+
{{ title }}
+
{{ text }}
+
+
+
+
+
+
+
+ Notable contributions
+
+
+
+
+
+
+
+ Highlights
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
{{ text }}
+
+
+
+
+
+
+
+
+
+ Languages used
+
+
+
+
+
+ {{ name }}
+ ({{ format("number", value, {style:"percent", maximumFractionDigits:2})}})
+
+
{{ format("number", size) }} byte{{ "s" }}
+
+
+
+ No languages used
+
+
+
+
+
+
+
+
+ Commits calendar
+
+
+
+
+
+
+
+
+
+ Recent activity
+
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
{{ event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" }} of
{{ repo }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ Pushed {{ event.size }} commit{{ "s" }} in
{{ repo }} on branch {{ event.branch }}
+
+
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ event.action === "opened" ? "Opened" : event.action === "merged" ? "Merged" : "Closed" }}
#{{ event.number }} {{ event.title }} in
{{ repo }}
+
+
+ {{ event.files.changed }} file{{ "s" }} changed ++{{ event.lines.added }} --{{ event.lines.deleted }}
+
+
+
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ Updated {{ event.pages.length }} wiki page{{ "s" }} in
{{ repo }}
+
+
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+
Created new {{ event.ref.type }}
{{ event.ref.name }} in
{{ repo }}
+
+
+
+ {{ format("date", timestamp, {timeStyle:"short", dateStyle:"short"}) }}
+
+
+
+
Deleted {{ event.ref.type }}
{{ event.ref.name }} from
{{ repo }}
+
+
+
+
+
+ No recent activity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/app/web/statics/about/script.js b/source/app/web/statics/about/script.js
new file mode 100644
index 00000000..b393edcd
--- /dev/null
+++ b/source/app/web/statics/about/script.js
@@ -0,0 +1,113 @@
+;(async function() {
+ //Init
+ const {data:version} = await axios.get("/.version")
+ const {data:hosted} = await axios.get("/.hosted")
+ //App
+ return new Vue({
+ //Initialization
+ el:"main",
+ async mounted() {
+ //Palette
+ try {
+ this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
+ } catch (error) {}
+ //GitHub limit tracker
+ const {data:requests} = await axios.get("/.requests")
+ this.requests = requests
+ //Initialization
+ const user = location.pathname.split("/").pop()
+ if ((user)&&(user !== "about")) {
+ this.user = user
+ await this.search()
+ }
+ else
+ this.searchable = true
+ //Embed
+ this.embed = !!(new URLSearchParams(location.search).get("embed"))
+ },
+ //Watchers
+ watch:{
+ palette:{
+ immediate:true,
+ handler(current, previous) {
+ document.querySelector("body").classList.remove(previous)
+ document.querySelector("body").classList.add(current)
+ }
+ }
+ },
+ //Methods
+ methods:{
+ format(type, value, options) {
+ switch (type) {
+ case "number":
+ return new Intl.NumberFormat(navigator.lang, options).format(value)
+ case "date":
+ return new Intl.DateTimeFormat(navigator.lang, options).format(new Date(value))
+ }
+ return value
+ },
+ async search() {
+ try {
+ this.error = null
+ this.metrics = null
+ this.pending = true
+ this.metrics = (await axios.get(`/about/query/${this.user}`)).data
+ }
+ catch (error) {
+ this.error = error
+ }
+ finally {
+ this.pending = false
+ }
+ }
+ },
+ //Computed properties
+ computed:{
+ ranked() {
+ return this.metrics?.rendered.plugins.achievements.list.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type))
+ },
+ achievements() {
+ return this.metrics?.rendered.plugins.achievements.list.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automater|octonaut|infographile)/i.test(title))
+ },
+ isocalendar() {
+ return this.metrics?.rendered.plugins.isocalendar.svg
+ .replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
+ .replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
+ .replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
+ .replace(/#30a14e/gi, "var(--color-calendar-graph-day-L3-bg)")
+ .replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
+ },
+ languages() {
+ return this.metrics?.rendered.plugins.languages.favorites
+ },
+ activity() {
+ return this.metrics?.rendered.plugins.activity.events
+ },
+ contributions() {
+ return this.metrics?.rendered.plugins.notable.contributions
+ },
+ account() {
+ if (!this.metrics)
+ return null
+ const {login, name} = this.metrics.rendered.user
+ return {login, name, avatar:this.metrics.rendered.computed.avatar}
+ },
+ url() {
+ return `${window.location.protocol}//${window.location.host}/about/${this.user}`
+ },
+ },
+ //Data initialization
+ data:{
+ version,
+ hosted,
+ user:"",
+ embed:false,
+ searchable:false,
+ requests:{limit:0, used:0, remaining:0, reset:0},
+ palette:"light",
+ metrics:null,
+ pending:false,
+ error:null,
+ }
+ })
+})()
\ No newline at end of file
diff --git a/source/app/web/statics/about/style.css b/source/app/web/statics/about/style.css
new file mode 100644
index 00000000..9ce34089
--- /dev/null
+++ b/source/app/web/statics/about/style.css
@@ -0,0 +1,360 @@
+/* Containers */
+ .container {
+ padding: 0 1rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ max-width: 920px;
+ margin: auto;
+ }
+ .center {
+ align-items: center;
+ }
+
+/* Search */
+ .search {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ margin: 3rem 0 1rem;
+ }
+ .search h2 {
+ margin: 0;
+ padding: 0;
+ }
+ .search .about {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ .search .about small {
+ font-size: .8rem;
+ color: var(--color-text-secondary);
+ text-align: left;
+ }
+ .search .inputs {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+ .search .inputs > * {
+ margin: .25rem;
+ }
+ .search .inputs input {
+ flex-grow: 1;
+ }
+ .search .info {
+ color: var(--color-text-secondary);
+ margin-top: 1rem;
+ }
+ .search .info svg {
+ fill: currentColor;
+ }
+
+/* Contributions */
+ .contributions {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .contribution {
+ display: flex;
+ align-items: center;
+ border-radius: 6px;
+ margin: .25rem;
+ padding: .25rem .5rem;
+ padding-left: .25rem;
+ color: var(--color-text-primary) !important;
+ border: 1px solid var(--color-border-primary);
+ }
+
+ .contribution img {
+ height: 1.5rem;
+ width: 1.5rem;
+ margin-right: .5rem;
+ border-radius: 6px;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 1px var(--color-avatar-border);
+ }
+
+/* Languages */
+ .languages .list {
+ display: flex;
+ flex-wrap: wrap;
+ }
+ .languages .language {
+ width: 100%;
+ margin: 0 0 .5rem;
+ }
+ .languages .percent {
+ font-size: .8rem;
+ }
+ .languages .size {
+ display: flex;
+ font-size: .8rem;
+ color: var(--color-text-secondary);
+ }
+ .progress {
+ height: 8px;
+ border-radius: 6px;
+ outline: 1px solid transparent;
+ }
+
+/* Isocalendar */
+ .isocalendar .svg {
+ margin-top: 2rem;
+ }
+
+/* Activity */
+ .activity > ul {
+ padding: 0;
+ margin: 0;
+ list-style-type: none;
+ }
+ .activity > ul > li {
+ margin: 0 0 1rem;
+ }
+ .activity time {
+ font-size: .8rem;
+ color: var(--color-text-secondary);
+ }
+ .activity svg {
+ fill: var(--color-text-primary);
+ flex-shrink: 0;
+ margin: .25rem 0;
+ margin-right: .5rem;
+ }
+ .activity .event {
+ display: flex;
+ align-items: flex-start;
+ }
+ .activity .event ul {
+ font-size: .8rem;
+ }
+
+/* User */
+ .user {
+ display: flex;
+ justify-content: center;
+ margin: 2rem 0;
+ }
+ .user img {
+ border-radius: 50%;
+ height: 4.5rem;
+ width: 4.5rem;
+ margin: 0 1rem;
+ }
+ .user .info {
+ display: flex;
+ flex-direction: column;
+ }
+ .user .name {
+ font-size: 2rem;
+ }
+ .user .login {
+ font-size: 1.25rem;
+ }
+
+/* Ranked achievements */
+ .rankeds {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ .ranked {
+ flex: 1 1 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 2rem;
+ }
+ .ranked .text {
+ min-height: 2rem;
+ }
+ .ranked .icon {
+ transform: scale(1.75);
+ }
+ .ranked .info {
+ margin-top: 1rem;
+ text-align: center;
+ }
+ .ranked .title {
+ font-size: 1.5rem;
+ margin-top: .5rem;
+ }
+ .user-rank {
+ font-size: 1.5rem;
+ }
+ .total-rank {
+ opacity: .75;
+ }
+
+/* Achievements */
+ .achievements {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ .achievement {
+ display: flex;
+ margin: .5rem;
+ max-width: 18rem;
+ width: 100%;
+ }
+
+/* Icon gauges */
+ .gauge {
+ stroke-linecap: round;
+ fill: none;
+ color: #58A6FF;
+ }
+ .gauge-base, .gauge-arc {
+ stroke: currentColor;
+ stroke-width: 6;
+ }
+ .gauge-base {
+ stroke-opacity: .2;
+ }
+ .gauge-arc {
+ fill: none;
+ stroke-dashoffset: 0;
+ animation-delay: 250ms;
+ animation: animation-gauge 1s ease forwards
+ }
+ @keyframes animation-gauge {
+ from {
+ stroke-dasharray: 0 329;
+ }
+ }
+
+/* General and colors */
+ .icon {
+ margin: 0 .25rem;
+ width: 44px;
+ height: 44px;
+ }
+ .text {
+ font-size: 12px;
+ }
+ .info {
+ font-size: 14px;
+ color: #58A6FF;
+ }
+ .x .info {
+ color: #666666;
+ }
+ .x .gauge {
+ color: #B0B0B0;
+ }
+ .b .info {
+ color: #9D8FFF;
+ }
+ .b .gauge {
+ color: #9E91FF;
+ }
+ .a .info {
+ color: #D79533;
+ }
+ .a .gauge {
+ color: #E7BD69;
+ }
+ .s .info {
+ color: #FF0000;
+ }
+ .s .gauge {
+ color: #FF0000;
+ }
+ .s .icon {
+ filter: sepia() saturate(100);
+ }
+ .secret .info {
+ color: #FF76CD;
+ }
+ .secret .gauge {
+ color: #FF79D1;
+ }
+
+/* Header */
+ h2 {
+ display: flex;
+ align-items: center;
+ font-weight: normal;
+ }
+ h2 svg {
+ margin-right: .25rem;
+ fill: currentColor;
+ height: 1.25rem;
+ width: 1.25rem;
+ }
+
+/* Inputs */
+ label {
+ cursor: pointer;
+ }
+ label:hover {
+ background-color: var(--color-input-contrast-bg);
+ border-radius: 6px;
+ }
+
+ input[type=text], input[type=number], select {
+ background-color: var(--color-input-contrast-bg);
+ padding: .4rem .8rem;
+ color: var(--color-text-primary);
+ background-color: var(--color-input-bg);
+ border: 1px solid var(--color-input-border);
+ border-radius: 6px;
+ outline: none;
+ box-shadow: var(--color-shadow-inset);
+ }
+
+ input[type=text]:focus, input[type=number]:focus, select:focus {
+ background-color: var(--color-input-bg);
+ border-color: var(--color-state-focus-border);
+ outline: none;
+ box-shadow: var(--color-state-focus-shadow);
+ }
+
+ button {
+ color: var(--color-btn-primary-text);
+ background-color: var(--color-btn-primary-bg);
+ border-color: var(--color-btn-primary-border);
+ box-shadow: var(--color-btn-primary-shadow),var(--color-btn-primary-inset-shadow);
+ padding: .4rem .8rem;
+ border-radius: 6px;
+ font-weight: 500;
+ margin: .5rem 0;
+ cursor: pointer;
+ transition-duration: all .12s ease-out;
+ }
+
+ button[disabled] {
+ color: var(--color-btn-primary-disabled-text);
+ background-color: var(--color-btn-primary-disabled-bg);
+ border-color: var(--color-btn-primary-disabled-border);
+ }
+
+ button:focus {
+ outline: none;
+ }
+
+/* Media screen */
+ @media only screen and (min-width: 740px) {
+ .rankeds {
+ flex-direction: row;
+ margin: 4rem 0 2rem;
+ }
+ .ranked {
+ margin-bottom: 0;
+ }
+ .languages .language {
+ width: 25%;
+ }
+ .search {
+ width: 520px;
+ }
+ }
\ No newline at end of file
diff --git a/source/app/web/statics/app.js b/source/app/web/statics/app.js
index 90ce3086..bbeb0c37 100644
--- a/source/app/web/statics/app.js
+++ b/source/app/web/statics/app.js
@@ -48,6 +48,7 @@
data:{
version,
user:"",
+ mode:"metrics",
tab:"overview",
palette:"light",
requests:{limit:0, used:0, remaining:0, reset:0},
diff --git a/source/app/web/statics/index.html b/source/app/web/statics/index.html
index 4aaef4fa..839b66f0 100644
--- a/source/app/web/statics/index.html
+++ b/source/app/web/statics/index.html
@@ -24,9 +24,9 @@
-
-
- Overview
+
@@ -36,16 +36,20 @@
Markdown code
+
-
+
+
+
Repository
License
diff --git a/source/app/web/statics/style.css b/source/app/web/statics/style.css
index 31090a73..e5c85eca 100644
--- a/source/app/web/statics/style.css
+++ b/source/app/web/statics/style.css
@@ -6,6 +6,11 @@ body {
color: var(--color-text-primary);
}
+iframe {
+ width: 100%;
+ height: 100%;
+}
+
/* Header */
header {
display: flex;
diff --git a/source/plugins/achievements/index.mjs b/source/plugins/achievements/index.mjs
index 933c52dc..78aa21b7 100644
--- a/source/plugins/achievements/index.mjs
+++ b/source/plugins/achievements/index.mjs
@@ -222,7 +222,7 @@
title:"Verified",
text:"Registered a GPG key to sign commits",
icon:" ",
- rank:value ? "$" : "X", progress:1, value, unlock:new Date(unlock?.createdAt),
+ rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
})
}
@@ -235,7 +235,7 @@
title:"Explorer",
text:"Starred a topic on GitHub Explore",
icon:" ",
- rank:value ? "$" : "X", progress:1, value, unlock:new Date(unlock?.createdAt),
+ rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
})
}
@@ -248,7 +248,7 @@
title:"Automater",
text:"Use GitHub Actions to automate profile updates",
icon:" ",
- rank:value ? "$" : "X", progress:1, value, unlock:new Date(unlock?.createdAt),
+ rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
})
}
@@ -261,7 +261,7 @@
title:"Infographile",
text:"Fervent supporter of metrics",
icon:" ",
- rank:(value)&&(login === _login) ? "$" : "X", progress:1, value, unlock:new Date(unlock?.createdAt),
+ rank:(value)&&(login === _login) ? "$" : "X", progress:(value)&&(login === _login) ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
})
}
@@ -274,7 +274,7 @@
title:"Octonaut",
text:"Following octocat",
icon:" ",
- rank:(value)&&(login === _login) ? "$" : "X", progress:1, value, unlock:new Date(unlock?.createdAt),
+ rank:(value)&&(login === _login) ? "$" : "X", progress:(value)&&(login === _login) ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
})
}
diff --git a/source/plugins/languages/README.md b/source/plugins/languages/README.md
index 1f5bbdb0..e9bb8467 100644
--- a/source/plugins/languages/README.md
+++ b/source/plugins/languages/README.md
@@ -31,4 +31,5 @@ It is also possible to use a predefined set of colors from [colorsets.json](colo
plugin_languages_colors: "0:orange, javascript:#ff0000, ..." # Make most used languages orange and JavaScript red
plugin_languages_details: bytes-size, percentage # Additionally display total bytes size and percentage
plugin_languages_threshold: 2% # Hides all languages less than 2%
+ plugin_languages_limit: 8 # Display up to 8 languages
```
diff --git a/source/plugins/languages/index.mjs b/source/plugins/languages/index.mjs
index fcd946e0..9ed39d73 100644
--- a/source/plugins/languages/index.mjs
+++ b/source/plugins/languages/index.mjs
@@ -7,12 +7,14 @@
return null
//Load inputs
- let {ignored, skipped, colors, details, threshold} = imports.metadata.plugins.languages.inputs({data, account, q})
+ let {ignored, skipped, colors, details, threshold, limit} = imports.metadata.plugins.languages.inputs({data, account, q})
threshold = (Number(threshold.replace(/%$/, ""))||0)/100
+ if (!limit)
+ limit = Infinity
//Custom colors
const colorsets = JSON.parse(`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/colorsets.json`)}`)
- if (`${colors}` in colorsets)
+ if ((`${colors}` in colorsets)&&(limit <= 8))
colors = colorsets[`${colors}`]
colors = Object.fromEntries(decodeURIComponent(colors).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x).map(x => x.split(":").map(x => x.trim())))
console.debug(`metrics/compute/${login}/plugins > languages > custom colors ${JSON.stringify(colors)}`)
@@ -42,7 +44,7 @@
//Compute languages stats
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
- languages.favorites = Object.entries(languages.stats).sort(([_an, a], [_bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value/languages.total > threshold)
+ languages.favorites = Object.entries(languages.stats).sort(([_an, a], [_bn, b]) => b - a).slice(0, limit).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value/languages.total > threshold)
const visible = {total:Object.values(languages.favorites).map(({size}) => size).reduce((a, b) => a + b, 0)}
for (let i = 0; i < languages.favorites.length; i++) {
languages.favorites[i].value /= visible.total
diff --git a/source/plugins/languages/metadata.yml b/source/plugins/languages/metadata.yml
index ea0c27b2..548a1d9e 100644
--- a/source/plugins/languages/metadata.yml
+++ b/source/plugins/languages/metadata.yml
@@ -30,11 +30,21 @@ inputs:
default: ""
example: my-repo-1, my-repo-2, owner/repo-3 ...
+ # Number of languages to display
+ # Set to 0 to disable limitations
+ plugin_languages_limit:
+ description: Maximum number of languages to display
+ type: number
+ default: 8
+ min: 0
+ max: 8
+
# Overrides default languages colors
# Use `${n}:${color}` to change the color of the n-th most used language (e.g. "0:red" to make your most used language red)
# Use `${language}:${color}` to change the color of named language (e.g. "javascript:red" to make JavaScript language red, language case is ignored)
# Use a value from `colorsets.json` to use a predefined set of colors
# Both hexadecimal and named colors are supported
+ # This cannot be used when "plugin_languages_limit" is greater than 8
plugin_languages_colors:
description: Custom languages colors
type: array