Version 2.10 (#30)

This commit is contained in:
Simon Lecoq
2020-12-28 20:04:44 +01:00
committed by GitHub
parent 8135bf19a6
commit 159b0757a8
39 changed files with 922 additions and 559 deletions

View File

@@ -55,7 +55,8 @@
"languages.ignored":"",
"languages.skipped":"",
"pagespeed.detailed":false,
"habits.from":100,
"pagespeed.screenshot":false,
"habits.from":200,
"habits.days":14,
"habits.facts":true,
"habits.charts":false,
@@ -65,6 +66,8 @@
"posts.source":"dev.to",
"isocalendar.duration":"half-year",
"projects.limit":4,
"projects.repositories":"",
"topics.mode":"starred",
"topics.sort":"stars",
"topics.limit":12,
"tweets.limit":2,
@@ -78,6 +81,7 @@
descriptions:{
classic:"Classic template",
terminal:"Terminal template",
repository:"(hidden)",
},
},
generated:{

View File

@@ -28,7 +28,7 @@
<div class="step">
<h2>2. Select a template</h2>
<div class="templates">
<label v-for="template in templates.list" :key="template">
<label v-for="template in templates.list" :key="template" v-show="templates.descriptions[template] !== '(hidden)'">
<input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending">
{{ templates.descriptions[template] || template }}
</label>
@@ -78,6 +78,10 @@
Detailed PageSpeed report
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load">
</label>
<label>
Include a website screenshot
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load">
</label>
</div>
<div class="options-group" v-if="plugins.enabled.languages">
<h4>{{ plugins.descriptions.languages }}</h4>
@@ -134,6 +138,13 @@
</div>
<div class="options-group" v-if="plugins.enabled.topics">
<h4>{{ plugins.descriptions.topics }}</h4>
<label>
Topics display mode
<select v-model="plugins.options['topics.mode']" @change="load">
<option value="starred">Starred topics</option>
<option value="mastered">Known and mastered technologies</option>
</select>
</label>
<label>
Topics sorting
<select v-model="plugins.options['topics.sort']">
@@ -154,6 +165,10 @@
Number of projects to display
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load">
</label>
<label>
Repositories projects to display (comma separated)
<input type="text" v-model="plugins.options['projects.repositories']" @change="load">
</label>
</div>
</div>
</template>

View File

@@ -27,8 +27,9 @@
const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
throw new Error("unsupported template")
const {query, image, style, fonts} = conf.templates[template]
const data = {base:{}, config:{}}
const {image, style, fonts} = conf.templates[template]
const queries = conf.queries
const data = {base:{}, config:{}, errors:[], plugins:{}, computed:{}}
//Base parts
{
@@ -44,29 +45,39 @@
else {
//Query data from GitHub API
console.debug(`metrics/compute/${login} > graphql query`)
Object.assign(data, await graphql(query
.replace(/[$]login/, `"${login}"`)
.replace(/[$]repositories/, `${repositories}`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`)
))
Object.assign(data, await graphql(queries.common({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString()})))
//Query repositories from GitHub API
{
//Iterate through repositories
let cursor = null
let pushed = 0
do {
console.debug(`metrics/compute/${login} > retrieving repositories after ${cursor}`)
const {user:{repositories:{edges, nodes}}} = await graphql(queries.repositories({login, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, 100)}))
cursor = edges?.[edges?.length-1]?.cursor
data.user.repositories.nodes.push(...nodes)
pushed = nodes.length
} while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories))
//Limit repositories
console.debug(`metrics/compute/${login} > keeping only ${repositories} repositories`)
data.user.repositories.nodes.splice(repositories)
console.debug(`metrics/compute/${login} > loaded ${data.user.repositories.nodes.length} repositories`)
}
//Compute metrics
console.debug(`metrics/compute/${login} > compute`)
const computer = Templates[template].default || Templates[template]
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
const promised = await Promise.all(pending)
//Check plugins errors
{
const errors = promised.filter(({result = null}) => result?.error)
if (die) {
if (errors.length)
throw new Error(`${errors.length} error${s(errors.length)} found...`)
}
else {
console.warn(`${errors.length} error${s(errors.length)} found, ignoring...`)
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
if (errors.length) {
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
if (die)
throw new Error(`An error occured during rendering, dying`)
else
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
}
}
}
@@ -159,7 +170,7 @@
/** Placeholder generator */
function placeholder({data, conf, q}) {
//Proxifier
const proxify = (target) => typeof target === "object" ? new Proxy(target, {
const proxify = (target) => (typeof target === "object")&&(target) ? new Proxy(target, {
get(target, property) {
//Primitive conversion
if (property === Symbol.toPrimitive)
@@ -196,11 +207,11 @@
[key, proxify({
posts:{source:"########", list:new Array("posts.limit" in q ? Math.max(Number(q["posts.limit"])||0, 0) : 2).fill({title:"###### ###### ####### ######", date:"####"})},
music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})},
pagespeed:{detailed:!!q["pagespeed.detailed"], scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
pagespeed:{detailed:!!q["pagespeed.detailed"], screenshot:!!q["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null, scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
followup:{issues:{count:0}, pr:{count:0}},
habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}},
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
topics:{list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
topics:{mode:"topics.mode" in q ? q["topics.mode"] : "starred", list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
tweets:{profile:{username:"########", verified:false}, list:[...new Array("tweets.limit" in q ? Math.max(Number(q["tweets.limit"])||0, 0) : 2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]},
}[key]??{})]

View File

@@ -1,5 +1,5 @@
//Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) {
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
@@ -7,40 +7,22 @@
return null
//Retrieve gists from graphql api
console.debug(`metrics/compute/${login}/plugins > gists > querying api`)
const {user:{gists}} = await graphql(`
query Gists {
user(login: "${login}") {
gists(last: 100) {
totalCount
nodes {
stargazerCount
isFork
forks {
totalCount
}
comments {
totalCount
}
}
}
}
}
`
)
const {user:{gists}} = await graphql(queries.gists({login}))
//Iterate through gists
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`)
let stargazers = 0, forks = 0, comments = 0
let stargazers = 0, forks = 0, comments = 0, files = 0
for (const gist of gists.nodes) {
//Skip forks
if (gist.isFork)
continue
//Compute stars, forks and comments
//Compute stars, forks, comments and files count
stargazers += gist.stargazerCount
forks += gist.forks.totalCount
comments += gist.comments.totalCount
files += gist.files.length
}
//Results
return {totalCount:gists.totalCount, stargazers, forks, comments}
return {totalCount:gists.totalCount, stargazers, forks, files, comments}
}
//Handle errors
catch (error) {

View File

@@ -1,5 +1,5 @@
//Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) {
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
@@ -24,24 +24,7 @@
const calendar = {}
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(`
query Calendar {
user(login: "${login}") {
calendar:contributionsCollection(from: "${from.toISOString()}", to: "${to.toISOString()}") {
contributionCalendar {
weeks {
contributionDays {
contributionCount
color
date
}
}
}
}
}
}
`
)
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.calendar({login, from:from.toISOString(), to:to.toISOString()}))
calendar[name] = weeks
}
//Apply padding

View File

@@ -171,7 +171,7 @@
//Limit tracklist
if (limit > 0) {
console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`)
tracks = tracks.slice(0, limit)
tracks.splice(limit)
}
//Convert artworks to base64
console.debug(`metrics/compute/${login}/plugins > music > loading artworks`)

View File

@@ -6,7 +6,7 @@
if ((!enabled)||(!q.pagespeed)||(!data.user.websiteUrl))
return null
//Parameters override
let {"pagespeed.detailed":detailed = false} = q
let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false} = q
//Duration in days
detailed = !!detailed
//Format url if needed
@@ -18,12 +18,18 @@
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
const scores = new Map()
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)
console.debug(request.data)
const {score, title} = request.data.lighthouseResult.categories[category]
scores.set(category, {score, title})
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
//Perform audit
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)
console.debug(request.data)
const {score, title} = request.data.lighthouseResult.categories[category]
scores.set(category, {score, title})
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
//Store screenshot
if ((screenshot)&&(category === "performance")) {
result.screenshot = request.data.lighthouseResult.audits["final-screenshot"].details.data
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
}
}))
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
//Detailed metrics

View File

@@ -29,7 +29,7 @@
//Limit tracklist
if (limit > 0) {
console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`)
posts = posts.slice(0, limit)
posts.splice(limit)
}
//Results
return {source, list:posts}

View File

@@ -1,38 +1,34 @@
//Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) {
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.projects))
return null
//Parameters override
let {"projects.limit":limit = 4} = q
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)) ?? []
//Limit
limit = Math.max(1, Math.min(100, Number(limit)))
//Retrieve contribution calendar from graphql api
limit = Math.max(repositories.length, Math.min(100, Number(limit)))
//Retrieve user owned projects from graphql api
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
const {user:{projects}} = await graphql(`
query Projects {
user(login: "${login}") {
projects(last: ${limit}, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
name
updatedAt
progress {
doneCount
inProgressCount
todoCount
enabled
}
}
}
}
}
`
)
const {user:{projects}} = await graphql(queries.projects({login, limit}))
//Retrieve repositories projects from graphql api
for (const identifier of repositories) {
//Querying repository project
console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups
const {user:{repository:{project}}} = await graphql(queries["projects.repository"]({user, repository, id}))
//Adding it to projects list
console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
project.name = `${project.name} (${user}/${repository})`
projects.nodes.unshift(project)
projects.totalCount++
}
//Iterate through projects and format them
console.debug(`metrics/compute/${login}/plugins > posts > processing ${projects.nodes.length} projects`)
console.debug(`metrics/compute/${login}/plugins > projects > processing ${projects.nodes.length} projects`)
const list = []
for (const project of projects.nodes) {
//Format date
@@ -49,6 +45,9 @@
//Append
list.push({name:project.name, updated, progress:{enabled, todo, doing, done, total:todo+doing+done}})
}
//Limit
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
list.splice(limit)
//Results
return {list, totalCount:projects.totalCount}
}

View File

@@ -6,13 +6,15 @@
if ((!enabled)||(!q.topics))
return null
//Parameters override
let {"topics.sort":sort = "stars", "topics.limit":limit = 15} = q
let {"topics.sort":sort = "stars", "topics.mode":mode = "starred", "topics.limit":limit = (mode === "mastered" ? 0 : 15)} = q
//Shuffle
const shuffle = (sort === "random")
//Sort method
sort = {starred:"created", activity:"updated", stars:"stars", random:"created"}[sort] ?? "starred"
//Limit
limit = Math.max(1, Math.min(20, Number(limit)))
limit = Math.max(0, Math.min(20, Number(limit)))
//Mode
mode = ["starred", "mastered"].includes(mode) ? mode : "starred"
//Start puppeteer and navigate to topics
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
let topics = []
@@ -49,11 +51,10 @@
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
topics = imports.shuffle(topics)
}
//Limit topics
if (limit > 0) {
//Limit topics (starred mode)
if ((mode === "starred")&&(limit > 0)) {
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
const removed = topics.slice(limit)
topics = topics.slice(0, limit)
const removed = topics.splice(limit)
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
}
//Convert icons to base64
@@ -66,8 +67,18 @@
//Escape HTML description
topic.description = imports.htmlescape(topic.description)
}
//Filter topics with icon (mastered mode)
if (mode === "mastered") {
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
topics = topics.filter(({icon}) => icon)
}
//Limit topics (mastered mode)
if ((mode === "mastered")&&(limit > 0)) {
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
topics.splice(limit)
}
//Results
return {list:topics}
return {mode, list:topics}
}
//Handle errors
catch (error) {

View File

@@ -0,0 +1,15 @@
query Calendar {
user(login: "$login") {
calendar:contributionsCollection(from: "$from", to: "$to") {
contributionCalendar {
weeks {
contributionDays {
contributionCount
color
date
}
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
query Metrics {
user(login: $login) {
user(login: "$login") {
databaseId
name
login
@@ -11,45 +11,11 @@ query Metrics {
gists {
totalCount
}
repositories(last: $repositories, isFork: false, ownerAffiliations: OWNER) {
repositories(last: 0, isFork: false, ownerAffiliations: OWNER) {
totalCount
totalDiskUsage
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
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
packages {
@@ -75,7 +41,7 @@ query Metrics {
totalPullRequestContributions
totalPullRequestReviewContributions
}
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
calendar:contributionsCollection(from: "$calendar.from", to: "$calendar.to") {
contributionCalendar {
weeks {
contributionDays {

20
src/queries/gists.graphql Normal file
View File

@@ -0,0 +1,20 @@
query Gists {
user(login: "$login") {
gists(last: 100) {
totalCount
nodes {
stargazerCount
isFork
forks {
totalCount
}
files {
name
}
comments {
totalCount
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
query Projects {
user(login: "$login") {
projects(last: $limit, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
name
updatedAt
progress {
doneCount
inProgressCount
todoCount
enabled
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
query Projects {
user(login: "$user") {
repository(name: "$repository") {
project(number: $id) {
name
updatedAt
progress {
doneCount
inProgressCount
todoCount
enabled
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
query Metrics {
user(login: "$login") {
repositories($after first: $repositories, isFork: false, ownerAffiliations: OWNER, orderBy: {field: UPDATED_AT, direction: DESC}) {
edges {
cursor
}
nodes {
name
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 8) {
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
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
query Metrics {
user(login: "$login") {
repository(name: "$repo") {
name
createdAt
diskUsage
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 8) {
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
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
}

View File

@@ -10,8 +10,10 @@
const logger = log ? console.debug : () => null
logger(`metrics/setup > setup`)
const templates = "src/templates"
const queries = "src/queries"
const conf = {
templates:{},
queries:{},
settings:{},
statics:path.resolve("src/html"),
node_modules:path.resolve("node_modules"),
@@ -52,22 +54,21 @@
continue
logger(`metrics/setup > load template [${name}]`)
const files = [
`${templates}/${name}/query.graphql`,
`${templates}/${name}/image.svg`,
`${templates}/${name}/style.css`,
`${templates}/${name}/fonts.css`,
]
const [query, image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
conf.templates[name] = {query, image, style, fonts}
].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${templates}/${name}/`, `${templates}/classic/`)).map(file => path.resolve(file))
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
conf.templates[name] = {image, style, fonts}
logger(`metrics/setup > load template [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, {
get() {
logger(`metrics/setup > reload template [${name}]`)
const [query, image, style, fonts] = files.map(file => `${fs.readFileSync(path.resolve(file))}`)
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
logger(`metrics/setup > reload template [${name}] > success`)
return {query, image, style, fonts}
return {image, style, fonts}
}
})
}
@@ -78,6 +79,39 @@
conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8"))
}
//Load queries
if (fs.existsSync(path.resolve(queries))) {
for (const query of await fs.promises.readdir(queries)) {
//Cache queries
const name = query.replace(/[.]graphql$/, "")
logger(`metrics/setup > load query [${name}]`)
conf.queries[`_${name}`] = `${await fs.promises.readFile(path.resolve(`${queries}/${query}`))}`
logger(`metrics/setup > load query [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.queries, `_${name}`, {
get() {
logger(`metrics/setup > reload query [${name}]`)
const raw = `${fs.readFileSync(path.resolve(`${queries}/${query}`))}`
logger(`metrics/setup > reload query [${name}] > success`)
return raw
}
})
}
}
}
else {
logger(`metrics/setup > load queries from build`)
conf.queries = JSON.parse(Buffer.from(`<#queries>`, "base64").toString("utf8"))
}
//Create queries formatters
Object.keys(conf.queries).map(name => conf.queries[name.substring(1)] = (vars = {}) => {
let query = conf.queries[name]
for (const [key, value] of Object.entries(vars))
query = query.replace(new RegExp(`[$]${key}`, "g"), value)
return query
})
//Conf
logger(`metrics/setup > setup > success`)
return conf

View File

@@ -5,7 +5,7 @@
+ (!!base.repositories)*108
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
+ (!!plugins.followup)*68
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20 + (!!plugins.pagespeed?.screenshot)*330
+ (!!plugins.habits)*28 + (!!plugins.habits?.facts)*58 + (!!plugins.habits?.charts)*226
+ (!!plugins.languages)*96
+ (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0)
@@ -380,11 +380,19 @@
<% } else { %>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
<%= plugins.gists.stargazers %> Stargazer<%= s(plugins.gists.stargazers) %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"></path></svg>
<%= plugins.gists.files %> File<%= s(plugins.gists.files) %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg>
<%= plugins.gists.comments %> Comment<%= s(plugins.gists.comments) %>
</div>
</section>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
<%= plugins.gists.stargazers %> Stargazer<%= s(plugins.gists.stargazers) %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<%= plugins.gists.forks %> Fork<%= s(plugins.gists.forks) %>
@@ -497,6 +505,13 @@
</section>
</div>
<% } %>
<% if (plugins.pagespeed.screenshot) { %>
<div class="row">
<section>
<img class="screenshot" src="<%= plugins.pagespeed.screenshot %>" width="452" height="315"/>
</section>
</div>
<% } %>
<% } %>
<% } %>
@@ -585,7 +600,7 @@
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M14.184 1.143a1.75 1.75 0 00-2.502-.57L.912 7.916a1.75 1.75 0 00-.53 2.32l.447.775a1.75 1.75 0 002.275.702l11.745-5.656a1.75 1.75 0 00.757-2.451l-1.422-2.464zm-1.657.669a.25.25 0 01.358.081l1.422 2.464a.25.25 0 01-.108.35l-2.016.97-1.505-2.605 1.85-1.26zM9.436 3.92l1.391 2.41-5.42 2.61-.942-1.63 4.97-3.39zM3.222 8.157l-1.466 1a.25.25 0 00-.075.33l.447.775a.25.25 0 00.325.1l1.598-.769-.83-1.436zm6.253 2.306a.75.75 0 00-.944-.252l-1.809.87a.75.75 0 00-.293.253L4.38 14.326a.75.75 0 101.238.848l1.881-2.75v2.826a.75.75 0 001.5 0v-2.826l1.881 2.75a.75.75 0 001.238-.848l-2.644-3.863z"></path></svg>
Starred topics
<%= {starred:"Starred topics", mastered:"Mastered technologies and topics"}[plugins.topics.mode] %>
</h2>
<div class="row">
<% if (plugins.topics.error) { %>
@@ -596,13 +611,21 @@
</div>
</section>
<% } else { %>
<section>
<div class="topics fill-width">
<% for (const {name, description} of plugins.topics.list) { %>
<div class="label" title="<%= description %>"><%= name.toLocaleLowerCase() %></div>
<% } %>
</div>
</section>
<section>
<div class="topics fill-width">
<% if (plugins.topics.mode === "starred") { %>
<% for (const {name, description} of plugins.topics.list) { %>
<div class="label" title="<%= description %>"><%= name.toLocaleLowerCase() %></div>
<% } %>
<% } else if (plugins.topics.mode === "mastered") { %>
<% for (const {name, icon} of plugins.topics.list) { %>
<% if (icon) { %>
<img src="data:image/png;base64,<%= icon %>" width="24" height="24" alt="<%= name %>" title="<%= name %>"/>
<% } %>
<% } %>
<% } %>
</div>
</section>
<% } %>
</div>
</section>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -226,6 +226,13 @@
fill: #e53935;
}
.screenshot {
width: 452px;
height: 315px;
margin: 8px 14px 4px;
border-radius: 5px;
}
/* Music plugin */
.tracklist {
display: flex;
@@ -284,6 +291,11 @@
flex-wrap: wrap;
}
.topics img {
border-radius: 5px;
margin: 4px;
}
/* Tweets */
.tweet {
font-size: 13px;

View File

@@ -2,7 +2,7 @@
import common from "./../common.mjs"
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Common
await common(...arguments)
}

View File

@@ -1,10 +1,9 @@
/** Template common processor */
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Init
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
const avatar = imports.imgb64(data.user.avatarUrl)
data.plugins = {}
console.debug(`metrics/compute/${login} > formatting common metrics`)
//Timezone config
@@ -24,7 +23,7 @@
pending.push((async () => {
try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql}, plugins[name])
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries}, plugins[name])
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (${data.plugins[name] !== null ? "success" : "skipped"})`)
}
catch (error) {

View File

@@ -2,10 +2,12 @@
//Imports
import classic from "./classic/template.mjs"
import repository from "./repository/template.mjs"
import terminal from "./terminal/template.mjs"
//Exports
export default {
classic,
repository,
terminal,
}

View File

@@ -0,0 +1,220 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 0
+ (!!base.header)*42
+ (!!plugins.traffic)*18
+ (!!plugins.followup)*68
+ (!!base.metadata)*28
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
%>">
<defs><style><%= fonts %></style></defs>
<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">
<% if (errors.length) { %>
<section>
<div class="row">
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= errors.map(({error}) => error.message).join(", ") %>
</div>
</div>
</section>
<% } else { %>
<% if (base.header) { %>
<section>
<div class="row">
<section>
<div class="field <%= computed.cakeday ? 'cakeday' : '' %>">
<% if (computed.cakeday) { %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 1.5a1.25 1.25 0 100 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707zM2 2.75c0 .45.108.875.3 1.25h-.55A1.75 1.75 0 000 5.75v2c0 .698.409 1.3 1 1.582v4.918c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0015 14.25V9.332c.591-.281 1-.884 1-1.582v-2A1.75 1.75 0 0014.25 4h-.55a2.75 2.75 0 00-2.45-4c-.984 0-1.874.42-2.57 1.23A5.086 5.086 0 008 2.274a5.086 5.086 0 00-.68-1.042C6.623.42 5.733 0 4.75 0A2.75 2.75 0 002 2.75zM8.941 4h2.309a1.25 1.25 0 100-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793zm-1.84 1.5H1.75a.25.25 0 00-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-.149zm1.649 0V8h5.5a.25.25 0 00.25-.25v-2a.25.25 0 00-.25-.25h-5.5zm0 4h4.75v4.75a.25.25 0 01-.25.25h-4.5v-5zm-1.5 0v5h-4.5a.25.25 0 01-.25-.25V9.5h4.75z"></path></svg>
Created <%= computed.registration %>
<% } else { %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Created <%= computed.registration %>
<% } %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" d="M2.5 3.5c0-.133.058-.318.282-.55.227-.237.592-.484 1.1-.708C4.899 1.795 6.354 1.5 8 1.5c1.647 0 3.102.295 4.117.742.51.224.874.47 1.101.707.224.233.282.418.282.551 0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 5.205 9.646 5.5 8 5.5c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707-.224-.233-.282-.418-.282-.551zM1 3.5c0-.626.292-1.165.7-1.59.406-.422.956-.767 1.579-1.041C4.525.32 6.195 0 8 0c1.805 0 3.475.32 4.722.869.622.274 1.172.62 1.578 1.04.408.426.7.965.7 1.591v9c0 .626-.292 1.165-.7 1.59-.406.422-.956.767-1.579 1.041C11.476 15.68 9.806 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.62-1.579-1.04-.408-.426-.7-.965-.7-1.591v-9zM2.5 8V5.724c.241.15.503.286.779.407C4.525 6.68 6.195 7 8 7c1.805 0 3.475-.32 4.722-.869.275-.121.537-.257.778-.407V8c0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 9.705 9.646 10 8 10c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707C2.558 8.318 2.5 8.133 2.5 8zm0 2.225V12.5c0 .133.058.318.282.55.227.237.592.484 1.1.708 1.016.447 2.471.742 4.118.742 1.647 0 3.102-.295 4.117-.742.51-.224.874-.47 1.101-.707.224-.233.282-.418.282-.551v-2.275c-.241.15-.503.285-.778.406-1.247.549-2.917.869-4.722.869-1.805 0-3.475-.32-4.721-.869a6.236 6.236 0 01-.779-.406z"></path></svg>
<%= computed.diskUsage %> used
</div>
<% if (plugins.traffic) { %>
<div class="field <%= plugins.traffic.error ? 'error' : '' %>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
<% if (plugins.traffic.error) { %>
<%= plugins.traffic.error.message %>
<% } else { %>
<%= plugins.traffic.views.count %> view<%= s(plugins.traffic.views.count) %> in last two weeks
<% } %>
</div>
<% } %>
</section>
<section>
<div class="field calendar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <%= computed.calendar.length*15 %> 11" width="<%= computed.calendar.length*15 %>" height="16">
<g>
<% for (const [x, {color}] of Object.entries(computed.calendar)) { %>
<rect class="day" x="<%= x*15 %>" y="0" width="11" height="11" fill="<%= color %>" rx="2" ry="2" />
<% } %>
</g>
</svg>
</div>
<% if (plugins.lines) { %>
<div class="field <%= plugins.lines.error ? 'error' : '' %>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V4.664a.25.25 0 00-.073-.177l-2.914-2.914a.25.25 0 00-.177-.073H2.75zM1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V1.75zm7 1.5a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0V7h-1.5a.75.75 0 010-1.5h1.5V4A.75.75 0 018 3.25zm-3 8a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75z"></path></svg>
<% if (plugins.lines.error) { %>
<%= plugins.lines.error.message %>
<% } else { %>
<%= plugins.lines.added %> added, <%= plugins.lines.deleted %> removed
<% } %>
</div>
<% } %>
</section>
</div>
</section>
<% } %>
<% if (plugins.followup) { %>
<div class="row">
<section class="column">
<h3>Issues</h3>
<% if (plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.followup.error.message %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= plugins.followup.issues.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
<span class="no-wrap"><%= plugins.followup.issues.closed %> Closed</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.issues.open %> Open</span>
</div>
</div>
<% } %>
</section>
<section class="column">
<h3>Pull requests</h3>
<% if (plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.followup.error.message %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= plugins.followup.pr.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.pr.merged %> Merged</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.pr.open %> Open</span>
</div>
</div>
<% } %>
</section>
</div>
<% } %>
<% if (plugins.projects) { %>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
Active projects
</h2>
<div class="row">
<% if (plugins.projects.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.projects.error.message %>
</div>
</section>
<% } else { %>
<section>
<% for (const {name, updated, progress} of plugins.projects.list) { %>
<div class="row fill-width">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
<%= name %>
</div>
</section>
</div>
<div class="row">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Updated <%= updated %>
</div>
</section>
<% if (progress.enabled) { %>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
<%= [progress.done ? `${progress.done} done` : "", progress.doing ? `${progress.doing} doing` : "", progress.todo ? `${progress.todo} todo` : ""].filter(str => str).join(" · ") %>
</div>
</section>
<% } %>
</div>
<% if (progress.enabled) { %>
<div class="field center horizontal-wrap ">
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="project-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#project-bar)" x="0" y="0" width="<%= (progress.done/progress.total)*460 %>" height="8" fill="#28A745"/>
<rect mask="url(#project-bar)" x="<%= (progress.done/progress.total)*460 %>" y="0" width="<%= (progress.doing/progress.total)*460 %>" height="8" fill="#6F42C1"/>
<rect mask="url(#project-bar)" x="<%= ((progress.done+progress.doing)/progress.total)*460 %>" y="0" width="<%= (progress.todo/progress.total)*460 %>" height="8" fill="#d1d5da"/>
</svg>
</div>
<% } %>
<% } %>
</section>
<% } %>
</div>
</section>
<% } %>
<% } %>
<% if (base.metadata) { %>
<footer>
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
</footer>
<% } %>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,60 @@
//Imports
import common from "./../common.mjs"
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Check arguments
const {repo} = q
if (!repo) {
console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`)
data.errors.push({error:{message:`You must pass a "repo" argument to use this template`}})
return await common(...arguments)
}
//Retrieving single repository
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
const {user:{repository}} = await graphql(queries.repository({login, repo}))
data.user.repositories.nodes = [repository]
//Get commit activity
console.debug(`metrics/compute/${login}/${repo} > querying api for commits`)
const commits = []
for (let page = 0; page < 1; page++) {
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page})
if (!data.length) {
console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
break
}
commits.push(...data)
}
console.debug(`metrics/compute/${login}/${repo} > ${commits.length} commits loaded`)
//Override creation date and disk usage
data.user.createdAt = repository.createdAt
data.user.repositories.totalDiskUsage = repository.diskUsage
//Override contributions calendar
const days = 14
//Compute relative date for each contribution
const now = new Date()
now.setHours(0, 0, 0, 0)
const contributions = commits.map(({commit}) => Math.abs(Math.ceil((now - new Date(commit.committer.date))/(24*60*60*1000)))).slice(0, days)
//Count contributions per relative day
const calendar = new Array(days).fill(0)
for (const day of contributions)
calendar[day]++
const max = Math.max(...calendar)
//Override contributions calendar
data.user.calendar.contributionCalendar.weeks = calendar.map(commit => ({contributionDays:{color:commit ? `var(--color-calendar-graph-day-L${Math.ceil(commit/max/0.25)}-bg)` : "var(--color-calendar-graph-day-bg)"}}))
//Override plugins parameters
q["projects.limit"] = 0
//Common
await common(...arguments)
await Promise.all(pending)
//Reformat projects name
data.plugins.projects.list.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim())
}

View File

@@ -1,103 +0,0 @@
query Metrics {
user(login: $login) {
databaseId
name
login
createdAt
avatarUrl
websiteUrl
isHireable
twitterUsername
gists {
totalCount
}
repositories(last: $repositories, isFork: false, ownerAffiliations: OWNER) {
totalCount
totalDiskUsage
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
}
releases {
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
}
}
}

View File

@@ -2,7 +2,7 @@
import common from "./../common.mjs"
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Common
await common(...arguments)
//Disable optimization to keep white-spaces