Version 2.2
- Add new template "terminal" - Add feature to flush cache of user on server - Server app improvement - Created metrics common - Package json loaded in setup
This commit is contained in:
2
action/dist/index.js
vendored
2
action/dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metrics",
|
"name": "metrics",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
|
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
19
src/app.mjs
19
src/app.mjs
@@ -43,6 +43,7 @@
|
|||||||
const limiter = ratelimit({max:60, windowMs:60*1000})
|
const limiter = ratelimit({max:60, windowMs:60*1000})
|
||||||
const templates = [...new Set([conf.settings.templates.default, ...(conf.settings.templates.enabled.length ? Object.keys(Templates).filter(key => conf.settings.templates.enabled.includes(key)) : Object.keys(Templates))])]
|
const templates = [...new Set([conf.settings.templates.default, ...(conf.settings.templates.enabled.length ? Object.keys(Templates).filter(key => conf.settings.templates.enabled.includes(key)) : Object.keys(Templates))])]
|
||||||
const enabled = Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)
|
const enabled = Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)
|
||||||
|
const actions = {flush:new Map()}
|
||||||
app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||||
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||||
app.get("/favicon.ico", limiter, (req, res) => res.sendStatus(204))
|
app.get("/favicon.ico", limiter, (req, res) => res.sendStatus(204))
|
||||||
@@ -59,6 +60,23 @@
|
|||||||
const {style, placeholder} = conf.templates[template]
|
const {style, placeholder} = conf.templates[template]
|
||||||
res.status(200).json({style, placeholder})
|
res.status(200).json({style, placeholder})
|
||||||
})
|
})
|
||||||
|
app.get("/action.flush", limiter, async (req, res) => {
|
||||||
|
const {token, user} = req.query
|
||||||
|
if (token) {
|
||||||
|
if (actions.flush.has(token)) {
|
||||||
|
console.debug(`metrics/app/${login} > flushed cache`)
|
||||||
|
cache.del(actions.flush.get(token))
|
||||||
|
return res.sendStatus(200)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
||||||
|
actions.flush.set(token, user)
|
||||||
|
return res.json({token})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
//Metrics
|
//Metrics
|
||||||
app.get("/:login", ...middlewares, async (req, res) => {
|
app.get("/:login", ...middlewares, async (req, res) => {
|
||||||
@@ -84,6 +102,7 @@
|
|||||||
//Compute rendering
|
//Compute rendering
|
||||||
try {
|
try {
|
||||||
//Render
|
//Render
|
||||||
|
console.log(req.query)
|
||||||
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf})
|
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf})
|
||||||
//Cache
|
//Cache
|
||||||
if ((!debug)&&(cached))
|
if ((!debug)&&(cached))
|
||||||
|
|||||||
@@ -85,14 +85,15 @@
|
|||||||
<script src="/vue.min.js"></script>
|
<script src="/vue.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
;(async function() {
|
;(async function() {
|
||||||
|
const url = new URLSearchParams(window.location.search)
|
||||||
new Vue({
|
new Vue({
|
||||||
el:"main",
|
el:"main",
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.load()
|
await this.load()
|
||||||
},
|
},
|
||||||
data:{
|
data:{
|
||||||
user:"",
|
user:url.get("user") || "",
|
||||||
palette:"light",
|
palette:url.get("palette") || "light",
|
||||||
plugins:{
|
plugins:{
|
||||||
list:(await axios.get("/plugins.list")).data,
|
list:(await axios.get("/plugins.list")).data,
|
||||||
enabled:{},
|
enabled:{},
|
||||||
@@ -109,10 +110,11 @@
|
|||||||
templates:{
|
templates:{
|
||||||
list:(await axios.get("/templates.list")).data,
|
list:(await axios.get("/templates.list")).data,
|
||||||
loaded:{},
|
loaded:{},
|
||||||
selected:(await axios.get("/templates.list")).data[0],
|
selected:url.get("template") || (await axios.get("/templates.list")).data[0],
|
||||||
placeholder:"",
|
placeholder:"",
|
||||||
descriptions:{
|
descriptions:{
|
||||||
classic:"Classic template",
|
classic:"Classic template",
|
||||||
|
terminal:"Terminal template"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
generated:{
|
generated:{
|
||||||
@@ -129,7 +131,7 @@
|
|||||||
.filter(([key, value]) => value)
|
.filter(([key, value]) => value)
|
||||||
.map(([key, value]) => `${key}=${+value}`)
|
.map(([key, value]) => `${key}=${+value}`)
|
||||||
.join("&")
|
.join("&")
|
||||||
return `${window.location.href}${this.user}${plugins.length ? `?${plugins}` : ""}`
|
return `${window.location.protocol}//${window.location.host}/${this.user}?template=${this.templates.selected}${plugins.length ? `&${plugins}` : ""}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
@@ -144,10 +146,11 @@
|
|||||||
},
|
},
|
||||||
async generate() {
|
async generate() {
|
||||||
this.generated.pending = true
|
this.generated.pending = true
|
||||||
|
await axios.get(`/action.flush?&token=${(await axios.get(`/action.flush?user=${this.user}`)).data.token}`)
|
||||||
this.generated.content = this.serialize((await axios.get(this.url)).data)
|
this.generated.content = this.serialize((await axios.get(this.url)).data)
|
||||||
},
|
},
|
||||||
serialize(svg) {
|
serialize(svg) {
|
||||||
return `data:image/svg+xml;base64,${btoa(svg)}`
|
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
//Template
|
//Template
|
||||||
console.debug(`metrics/compute/${login} > compute`)
|
console.debug(`metrics/compute/${login} > compute`)
|
||||||
const computer = Templates[template].default || Templates[template]
|
const computer = Templates[template].default || Templates[template]
|
||||||
await computer({login, q}, {data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, imgb64}})
|
await computer({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, imgb64}})
|
||||||
await Promise.all(pending)
|
await Promise.all(pending)
|
||||||
console.debug(`metrics/compute/${login} > compute > success`)
|
console.debug(`metrics/compute/${login} > compute > success`)
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,22 @@
|
|||||||
if (conf.settings.debug)
|
if (conf.settings.debug)
|
||||||
logger(conf.settings)
|
logger(conf.settings)
|
||||||
|
|
||||||
|
//Load package settings
|
||||||
|
logger(`metrics/setup > load package.json`)
|
||||||
|
if (fs.existsSync(path.resolve("package.json"))) {
|
||||||
|
conf.package = JSON.parse(`${await fs.promises.readFile(path.resolve("package.json"))}`)
|
||||||
|
logger(`metrics/setup > load package.json > success`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger(`metrics/setup > load package.json > (missing)`)
|
||||||
|
conf.package = {version:"<#version>"}
|
||||||
|
}
|
||||||
|
|
||||||
//Load templates
|
//Load templates
|
||||||
if (fs.existsSync(path.resolve(templates))) {
|
if (fs.existsSync(path.resolve(templates))) {
|
||||||
for (const name of await fs.promises.readdir(templates)) {
|
for (const name of await fs.promises.readdir(templates)) {
|
||||||
//Cache templates
|
//Cache templates
|
||||||
if (/^index.mjs$/.test(name))
|
if (/.*[.]mjs$/.test(name))
|
||||||
continue
|
continue
|
||||||
logger(`metrics/setup > load template [${name}]`)
|
logger(`metrics/setup > load template [${name}]`)
|
||||||
const files = [
|
const files = [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
query Metrics {
|
query Metrics {
|
||||||
user(login: $login) {
|
user(login: $login) {
|
||||||
|
databaseId
|
||||||
name
|
name
|
||||||
login
|
login
|
||||||
createdAt
|
createdAt
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
}
|
}
|
||||||
.field.error svg {
|
.field.error svg {
|
||||||
fill: #cb2431;
|
fill: #cb2431;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Displays */
|
/* Displays */
|
||||||
|
|||||||
@@ -1,52 +1,7 @@
|
|||||||
/** Template processort */
|
//Imports
|
||||||
export default async function ({login, q}, {data, rest, graphql, plugins}, {s, pending, imports}) {
|
import common from "./../common.mjs"
|
||||||
|
|
||||||
//Init
|
|
||||||
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, svg:{height:355, width:480}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}, plugins:{}}
|
|
||||||
const avatar = imports.imgb64(data.user.avatarUrl)
|
|
||||||
|
|
||||||
//Plugins
|
|
||||||
if (data.user.websiteUrl)
|
|
||||||
imports.plugins.pagespeed({login, url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed)
|
|
||||||
imports.plugins.lines({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.lines)
|
|
||||||
imports.plugins.traffic({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.traffic)
|
|
||||||
imports.plugins.habits({login, rest, computed, pending, q}, plugins.habits)
|
|
||||||
imports.plugins.selfskip({login, rest, computed, pending, q}, plugins.selfskip)
|
|
||||||
imports.plugins.languages({login, data, computed, pending, q}, plugins.languages)
|
|
||||||
imports.plugins.followup({login, data, computed, pending, q}, plugins.followup)
|
|
||||||
|
|
||||||
//Iterate through user's repositories
|
|
||||||
for (const repository of data.user.repositories.nodes) {
|
|
||||||
//Simple properties with totalCount
|
|
||||||
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged"])
|
|
||||||
computed.repositories[property] += repository[property].totalCount
|
|
||||||
//Forks
|
|
||||||
computed.repositories.forks += repository.forkCount
|
|
||||||
//License
|
|
||||||
if (repository.licenseInfo)
|
|
||||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] || 0) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
//Compute licenses stats
|
|
||||||
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || ""
|
|
||||||
|
|
||||||
//Compute total commits and sponsorships
|
|
||||||
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
|
||||||
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
|
|
||||||
|
|
||||||
//Compute registration date
|
|
||||||
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
|
||||||
const years = Math.floor(diff)
|
|
||||||
const months = Math.ceil((diff-years)*12)
|
|
||||||
computed.registration = years ? `${years} year${s(years)} ago` : `${months} month${s(months)} ago`
|
|
||||||
|
|
||||||
//Compute calendar
|
|
||||||
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
|
||||||
|
|
||||||
//Avatar (base64)
|
|
||||||
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
||||||
|
|
||||||
//Token scopes
|
|
||||||
computed.token.scopes = (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
|
|
||||||
|
|
||||||
|
/** Template processor */
|
||||||
|
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
|
||||||
|
await common(...arguments)
|
||||||
}
|
}
|
||||||
52
src/templates/common.mjs
Normal file
52
src/templates/common.mjs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/** Template common processor */
|
||||||
|
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
|
||||||
|
|
||||||
|
//Init
|
||||||
|
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, svg:{height:355, width:480}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}, plugins:{}}
|
||||||
|
const avatar = imports.imgb64(data.user.avatarUrl)
|
||||||
|
|
||||||
|
//Plugins
|
||||||
|
if (data.user.websiteUrl)
|
||||||
|
imports.plugins.pagespeed({login, url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed)
|
||||||
|
imports.plugins.lines({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.lines)
|
||||||
|
imports.plugins.traffic({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.traffic)
|
||||||
|
imports.plugins.habits({login, rest, computed, pending, q}, plugins.habits)
|
||||||
|
imports.plugins.selfskip({login, rest, computed, pending, q}, plugins.selfskip)
|
||||||
|
imports.plugins.languages({login, data, computed, pending, q}, plugins.languages)
|
||||||
|
imports.plugins.followup({login, data, computed, pending, q}, plugins.followup)
|
||||||
|
|
||||||
|
//Iterate through user's repositories
|
||||||
|
for (const repository of data.user.repositories.nodes) {
|
||||||
|
//Simple properties with totalCount
|
||||||
|
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged"])
|
||||||
|
computed.repositories[property] += repository[property].totalCount
|
||||||
|
//Forks
|
||||||
|
computed.repositories.forks += repository.forkCount
|
||||||
|
//License
|
||||||
|
if (repository.licenseInfo)
|
||||||
|
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
//Compute licenses stats
|
||||||
|
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || ""
|
||||||
|
|
||||||
|
//Compute total commits and sponsorships
|
||||||
|
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||||
|
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
|
||||||
|
|
||||||
|
//Compute registration date
|
||||||
|
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
||||||
|
const years = Math.floor(diff)
|
||||||
|
const months = Math.ceil((diff-years)*12)
|
||||||
|
computed.registration = years ? `${years} year${s(years)} ago` : `${months} month${s(months)} ago`
|
||||||
|
|
||||||
|
//Compute calendar
|
||||||
|
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
||||||
|
|
||||||
|
//Avatar (base64)
|
||||||
|
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||||
|
|
||||||
|
//Token scopes
|
||||||
|
computed.token.scopes = (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
//Imports
|
//Imports
|
||||||
import classic from "./classic/template.mjs"
|
import classic from "./classic/template.mjs"
|
||||||
|
import terminal from "./terminal/template.mjs"
|
||||||
|
|
||||||
//Exports
|
//Exports
|
||||||
export default {
|
export default {
|
||||||
classic
|
classic,
|
||||||
|
terminal,
|
||||||
}
|
}
|
||||||
124
src/templates/terminal/image.svg
Normal file
124
src/templates/terminal/image.svg
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="<%= computed.svg.width %>" height="<%= computed.svg.height %>">
|
||||||
|
<style>
|
||||||
|
<%= style %>
|
||||||
|
.stdin, .stdout {
|
||||||
|
animation-duration: .2s;
|
||||||
|
}
|
||||||
|
.stdout {
|
||||||
|
animation-duration: .2s;
|
||||||
|
}
|
||||||
|
<% for (let i = 0; i < 12; i++) { %>
|
||||||
|
.stdin:nth-of-type(<%= i+1 %>) {
|
||||||
|
animation-delay: <%= i*.2 %>s;
|
||||||
|
}
|
||||||
|
.stdout:nth-of-type(<%= i+2 %>) {
|
||||||
|
animation-delay: <%= (i+1)*.2 %>s;
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
|
footer {
|
||||||
|
animation-delay: <%= 12*.2 %>s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">GitHub metrics</span>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button">─</div>
|
||||||
|
<div class="button">□</div>
|
||||||
|
<div class="button exit">✕</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><span class="banner">GitHub metrics generator <%= meta.version %>
|
||||||
|
These generated metrics comes with ABSOLUTELY NO WARRANTY,
|
||||||
|
to the extent permitted by applicable law.
|
||||||
|
|
||||||
|
Last generated: <%= new Date().toGMTString() %>
|
||||||
|
</span>
|
||||||
|
<div class="stdin"><%- meta.$ %> whoami</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><b><%= user.name || user.login %></b> registered=<%= computed.registration %>, uid=<%= `${user.databaseId}`.substr(-4) %>, gid=<%= user.organizations.totalCount %>, followers=<%= user.followers.totalCount %>
|
||||||
|
contributed to <%= user.repositoriesContributedTo.totalCount %> repositor<%= s(user.repositoriesContributedTo.totalCount, "y") %> <b><% for (const [x, {color}] of Object.entries(computed.calendar)) { -%><span style="color:<%= color %>">#</span><% } %></b>
|
||||||
|
</div>
|
||||||
|
<div class="stdin"><%- meta.$ %> git status</div><!--
|
||||||
|
--><div class="stdout"><b>Recent activity</b><!--
|
||||||
|
--><b><%= `${computed.commits}`.padStart(5) %></b> commit<%= s(computed.commits) %>
|
||||||
|
<b><%= `${user.contributionsCollection.totalPullRequestReviewContributions}`.padStart(5) %></b> pull request<%= s(user.contributionsCollection.totalPullRequestReviewContributions) %> reviewed
|
||||||
|
<b><%= `${user.contributionsCollection.totalPullRequestContributions}`.padStart(5) %></b> pull request<%= s(user.contributionsCollection.totalPullRequestContributions) %> opened
|
||||||
|
<b><%= `${user.contributionsCollection.totalIssueContributions}`.padStart(5) %></b> issue<%= s(user.contributionsCollection.totalIssueContributions) %> opened
|
||||||
|
<b><%= `${user.issueComments.totalCount}`.padStart(5) %></b> issue comment<%= s(user.issueComments.totalCount) %>
|
||||||
|
|
||||||
|
<b>Tracked activity</b>
|
||||||
|
<b><%= `${user.following.totalCount}`.padStart(5) %></b> user<%= s(user.followers.totalCount) %> followed
|
||||||
|
<b><%= `${computed.sponsorships}`.padStart(5) %></b> repositor<%= s(computed.sponsorships, "y") %> sponsored
|
||||||
|
<b><%= `${user.starredRepositories.totalCount}`.padStart(5) %></b> repositor<%= s(user.starredRepositories.totalCount, "y") %> starred
|
||||||
|
<b><%= `${user.watching.totalCount}`.padStart(5) %></b> repositor<%= s(user.watching.totalCount, "y") %> watched
|
||||||
|
<% if (computed.plugins.lines) { -%><% if (computed.plugins.lines.error) { -%>
|
||||||
|
|
||||||
|
<span class="diff error">@@ <%= computed.plugins.lines.error %> @@</span>
|
||||||
|
<% } else { -%>
|
||||||
|
|
||||||
|
<span class="diff">@@ -<%= computed.plugins.lines.deleted %> +<%= computed.plugins.lines.added %> @@</span>
|
||||||
|
<% }} -%></div>
|
||||||
|
<div class="stdin"><%- meta.$ %> ls -lh github/repositories</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
-->Total <%= user.repositories.totalCount %> repositor<%= s(user.repositories.totalCount, "y") %>
|
||||||
|
<% if (computed.plugins.traffic) { -%><% if (computed.plugins.traffic.error) { -%>
|
||||||
|
---- <b> </b> views <span class="error">(<%= computed.plugins.traffic.error %>)</span>
|
||||||
|
<% } else { -%>
|
||||||
|
-r-- <b><%= `${computed.plugins.traffic.views.count}`.padStart(5) %></b> views
|
||||||
|
<% }} -%>
|
||||||
|
-r-- <b><%= `${computed.repositories.stargazers}`.padStart(5) %></b> stargazer<%= s(computed.repositories.stargazers) %>
|
||||||
|
-r-- <b><%= `${computed.repositories.forks}`.padStart(5) %></b> fork<%= s(computed.repositories.forks) %>
|
||||||
|
-r-- <b><%= `${computed.repositories.watchers}`.padStart(5) %></b> watcher<%= s(computed.repositories.watchers) %>
|
||||||
|
dr-x <b><%= `${user.packages.totalCount}`.padStart(5) %></b> package<%= s(user.packages.totalCount) %>
|
||||||
|
dr-x <b><%= `${user.gists.totalCount}`.padStart(5) %></b> gist<%= s(user.gists.totalCount) %>
|
||||||
|
<% if (computed.plugins.followup) { -%><% if (computed.plugins.followup.error) { -%>
|
||||||
|
d--- <b> </b> ISSUES <span class="error">(<%= computed.plugins.followup.error %>)</span>
|
||||||
|
d--- <b> </b> PULL_REQUESTS <span class="error">(<%= computed.plugins.followup.error %>)</span>
|
||||||
|
<% } else { -%>
|
||||||
|
dr-x <b><%= `${computed.plugins.followup.issues.count}`.padStart(5) %></b> ISSUES
|
||||||
|
-r-- <b><%= `${computed.plugins.followup.issues.open}`.padStart(5) %></b> ├── open
|
||||||
|
-r-- <b><%= `${computed.plugins.followup.issues.closed}`.padStart(5) %></b> └── closed
|
||||||
|
dr-x <b><%= `${computed.plugins.followup.issues.count}`.padStart(5) %></b> PULL_REQUESTS
|
||||||
|
-r-- <b><%= `${computed.plugins.followup.pr.open}`.padStart(5) %></b> ├── open
|
||||||
|
-r-- <b><%= `${computed.plugins.followup.pr.merged}`.padStart(5) %></b> └── merged
|
||||||
|
<% }} -%>
|
||||||
|
<% if (computed.licenses.favorite.length) { -%>
|
||||||
|
dr-x LICENSE
|
||||||
|
-r-- └── <%= computed.licenses.favorite %>
|
||||||
|
<% } -%>
|
||||||
|
</div><% if (computed.plugins.languages) { -%><% if (computed.plugins.languages.error) { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><%- meta.$ %> locale</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><span class="error"><%= computed.plugins.languages.error %></span>
|
||||||
|
<% } else { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><%- meta.$ %> locale</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><% for (const {name, value} of computed.plugins.languages.favorites) { -%>
|
||||||
|
<b><%= name.toLocaleUpperCase().padEnd(12) %></b> [<%= "#".repeat(Math.ceil(100*value/5)).padEnd(20) %>] <%= (100*value).toFixed(2).padEnd(5) %>%
|
||||||
|
<% }} -%></div><% } -%><% if (computed.plugins.pagespeed) { -%><% if (computed.plugins.pagespeed.error) { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><%- meta.$ %> curl -I <%= user.websiteUrl %></div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><span class="error"><%= computed.plugins.pagespeed.error %></span>
|
||||||
|
<% } else { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><%- meta.$ %> curl -I <%= user.websiteUrl %></div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><b>User-Agent</b>: Google PageSpeed API
|
||||||
|
<b>Location</b>: <%= user.websiteUrl %>
|
||||||
|
<% for (const {score, title} of computed.plugins.pagespeed.scores) { -%>
|
||||||
|
<b><%= `X-${title.replace(/ /g, "-")}` %></b>: <%= Math.round(score*100) %>%
|
||||||
|
<% }} -%></div><% } -%>
|
||||||
|
|
||||||
|
<footer>Connection reset by 127.0.0.1</footer></pre>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 6.6 KiB |
88
src/templates/terminal/placeholder.svg
Normal file
88
src/templates/terminal/placeholder.svg
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 640 + 80 + (!!plugins.followup)*(100+32) + (!!plugins.languages)*(170+32) + (!!plugins.lines)*(30+32) + (!!plugins.traffic)*(16+32) + (!!plugins.pagespeed)*(136+32) %>">
|
||||||
|
<style>
|
||||||
|
<%= style %>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">GitHub metrics</span>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="button">─</div>
|
||||||
|
<div class="button">□</div>
|
||||||
|
<div class="button exit">✕</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><span class="banner">GitHub metrics generator X.Y.Z
|
||||||
|
These generated metrics comes with ABSOLUTELY NO WARRANTY,
|
||||||
|
to the extent permitted by applicable law.
|
||||||
|
|
||||||
|
Last generated: <%= new Date().toGMTString() %>
|
||||||
|
</span>
|
||||||
|
<div class="stdin"><span class="ps1-path">▇▇▇▇@metrics</span>:<span class="ps1-location">~</span># whoami</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><b>▇▇▇▇</b> registered=▇▇, uid=▇▇, gid=▇▇, followers=▇▇
|
||||||
|
contributed to ▇▇ repositories <b style="color:#ebedf0">##############</b>
|
||||||
|
</div>
|
||||||
|
<div class="stdin"><span class="ps1-path">▇▇▇▇@metrics</span>:<span class="ps1-location">~</span># git status</div><!--
|
||||||
|
--><div class="stdout"><b>Recent activity</b><!--
|
||||||
|
--><b><%= `▇▇`.padStart(5) %></b> commits
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> pull requests reviewed
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> pull requests opened
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> issues opened
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> issue comments
|
||||||
|
|
||||||
|
<b>Tracked activity</b>
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> users followed
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> repositories sponsored
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> repositories starred
|
||||||
|
<b><%= `▇▇`.padStart(5) %></b> repositories watched
|
||||||
|
<% if (plugins.lines) { -%>
|
||||||
|
|
||||||
|
<span class="diff">@@ -▇▇ +▇▇ @@</span>
|
||||||
|
<% } -%></div>
|
||||||
|
<div class="stdin"><span class="ps1-path">▇▇▇▇@metrics</span>:<span class="ps1-location">~</span># ls -lh github/repositories</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
-->Total ▇▇ repositories
|
||||||
|
<% if (plugins.traffic) { -%>
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> views
|
||||||
|
<% } -%>
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> stargazers
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> forks
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> watchers
|
||||||
|
dr-x <b><%= `▇▇`.padStart(5) %></b> packages
|
||||||
|
dr-x <b><%= `▇▇`.padStart(5) %></b> gists
|
||||||
|
<% if (plugins.followup) { -%>
|
||||||
|
dr-x <b><%= `▇▇`.padStart(5) %></b> ISSUES
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> ├── open
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> └── closed
|
||||||
|
dr-x <b><%= `▇▇`.padStart(5) %></b> PULL_REQUESTS
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> ├── open
|
||||||
|
-r-- <b><%= `▇▇`.padStart(5) %></b> └── merged
|
||||||
|
<% } -%>
|
||||||
|
dr-x LICENSE
|
||||||
|
-r-- └── ▇▇▇▇
|
||||||
|
</div><% if (plugins.languages) { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><span class="ps1-path">▇▇▇▇@metrics</span>:<span class="ps1-location">~</span># locale</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><% for (const value of [0.6, 0.2, 0.1, 0.05, 0.05, 0, 0, 0]) { -%>
|
||||||
|
<b><%= `▇▇▇▇▇▇`.padEnd(12) %></b> [<%= "#".repeat(Math.ceil(100*value/5)).padEnd(20) %>] ▇%
|
||||||
|
<% } -%></div><% } -%><% if (plugins.pagespeed) { -%>
|
||||||
|
|
||||||
|
<div class="stdin"><span class="ps1-path">▇▇▇▇@metrics</span>:<span class="ps1-location">~</span># curl -I ▇▇▇▇▇▇</div><!--
|
||||||
|
--><div class="stdout"><!--
|
||||||
|
--><b>User-Agent</b>: Google PageSpeed API
|
||||||
|
<b>Location</b>: ▇▇▇▇▇▇
|
||||||
|
<b>X-Performance</b>: ▇%
|
||||||
|
<b>X-Accessibility</b>: ▇%
|
||||||
|
<b>X-Best-Practices</b>: ▇%
|
||||||
|
<b>X-SEO</b>: ▇%</div><% } -%>
|
||||||
|
|
||||||
|
<footer>Connection reset by 127.0.0.1</footer></pre>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
97
src/templates/terminal/query.graphql
Normal file
97
src/templates/terminal/query.graphql
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
query Metrics {
|
||||||
|
user(login: $login) {
|
||||||
|
databaseId
|
||||||
|
name
|
||||||
|
login
|
||||||
|
createdAt
|
||||||
|
avatarUrl
|
||||||
|
websiteUrl
|
||||||
|
gists {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
repositories(last: $repositories, isFork: false, ownerAffiliations: OWNER) {
|
||||||
|
totalCount
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
watchers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
stargazers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
languages(first: 4) {
|
||||||
|
edges {
|
||||||
|
size
|
||||||
|
node {
|
||||||
|
color
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issues_open: issues(states: OPEN) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
issues_closed: issues(states: CLOSED) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
pr_open: pullRequests(states: OPEN) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
pr_merged: pullRequests(states: MERGED) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
forkCount
|
||||||
|
licenseInfo {
|
||||||
|
spdxId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packages {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
starredRepositories {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
watching {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
sponsorshipsAsSponsor {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
sponsorshipsAsMaintainer {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
contributionsCollection {
|
||||||
|
totalRepositoriesWithContributedCommits
|
||||||
|
totalCommitContributions
|
||||||
|
restrictedContributionsCount
|
||||||
|
totalIssueContributions
|
||||||
|
totalPullRequestContributions
|
||||||
|
totalPullRequestReviewContributions
|
||||||
|
}
|
||||||
|
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
|
||||||
|
contributionCalendar {
|
||||||
|
weeks {
|
||||||
|
contributionDays {
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositoriesContributedTo {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
followers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
following {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
issueComments {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
organizations {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/templates/terminal/style.css
Normal file
107
src/templates/terminal/style.css
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/* SVG global context */
|
||||||
|
svg {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title bar */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
background: linear-gradient(#504b45 0%,#3c3b37 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #d5d0ce;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 8px;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background: linear-gradient(#7d7871 0%, #595953 100%);
|
||||||
|
text-shadow: 0px 1px 0px rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.exit {
|
||||||
|
background: linear-gradient(#f37458 0%, #de4c12 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal */
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
background: #42092B;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner, footer {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prompt */
|
||||||
|
.ps1-path {
|
||||||
|
color: #7EDA29;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps1-location {
|
||||||
|
color: #4878c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diff */
|
||||||
|
.diff {
|
||||||
|
color: #3A96DD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
.error {
|
||||||
|
color: #cb2431;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
.stdin, footer {
|
||||||
|
width: 0%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
animation-name: stdin-animation;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stdout {
|
||||||
|
max-height: 0%;
|
||||||
|
overflow: hidden;
|
||||||
|
animation-name: stdout-animation;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stdin-animation {
|
||||||
|
0% { width: 0%; }
|
||||||
|
100% { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stdout-animation {
|
||||||
|
0% { max-height: 0; }
|
||||||
|
100% { max-height: 360px; }
|
||||||
|
}
|
||||||
34
src/templates/terminal/template.mjs
Normal file
34
src/templates/terminal/template.mjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//Imports
|
||||||
|
import common from "./../common.mjs"
|
||||||
|
|
||||||
|
/** Template processor */
|
||||||
|
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
|
||||||
|
//Common
|
||||||
|
await common(...arguments)
|
||||||
|
const computed = data.computed
|
||||||
|
|
||||||
|
//Compute image size
|
||||||
|
computed.svg = {height:640, width:480}
|
||||||
|
if (computed.plugins.followup)
|
||||||
|
computed.svg.height += 100 + 32
|
||||||
|
if (computed.plugins.lines)
|
||||||
|
computed.svg.height += 30 + 32
|
||||||
|
if (computed.plugins.traffic)
|
||||||
|
computed.svg.height += 16 + 32
|
||||||
|
if (computed.plugins.pagespeed)
|
||||||
|
computed.svg.height += 136 + 32
|
||||||
|
if (computed.plugins.languages)
|
||||||
|
computed.svg.height += 170 + 32
|
||||||
|
|
||||||
|
//Compute registration date
|
||||||
|
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
||||||
|
const years = Math.floor(diff)
|
||||||
|
const months = Math.ceil((diff-years)*12)
|
||||||
|
computed.registration = years ? `${years}y` : `${months}m`
|
||||||
|
|
||||||
|
//Meta
|
||||||
|
data.meta = {
|
||||||
|
version:conf.package.version,
|
||||||
|
$:`<span class="ps1-path">${data.user.login}@metrics</span>:<span class="ps1-location">~</span>${computed.token.scopes.includes("repo") ? "#" : "$"}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
//Perform tests
|
//Perform tests
|
||||||
await test.build()
|
await test.build()
|
||||||
for (const template of [
|
for (const template of [
|
||||||
"classic"
|
"classic",
|
||||||
|
"terminal",
|
||||||
]) {
|
]) {
|
||||||
for (const q of [
|
for (const q of [
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
const assets = {}
|
const assets = {}
|
||||||
const templates = path.join(__dirname, "..", "src/templates")
|
const templates = path.join(__dirname, "..", "src/templates")
|
||||||
for (const name of await fs.promises.readdir(templates)) {
|
for (const name of await fs.promises.readdir(templates)) {
|
||||||
if (/^index.mjs$/.test(name))
|
if (/.*[.]mjs$/.test(name))
|
||||||
continue
|
continue
|
||||||
console.log(`Including template ${name}`)
|
console.log(`Including template ${name}`)
|
||||||
const files = [
|
const files = [
|
||||||
|
|||||||
Reference in New Issue
Block a user