Fix svg height, add conversion options and options to use differents data from GitHub account (#35)
* Display all features on web instance but disable them when they're not enabled * Resize dynamically SVG output and add support to convert images to jpeg/png * Disable animations when computing height * Add option to disable animations * Add options to specify different data from used GitHub account * Update tests
This commit is contained in:
34
action.yml
34
action.yml
@@ -38,6 +38,20 @@ inputs:
|
||||
description: Timezone used by metrics
|
||||
default: ""
|
||||
|
||||
# Specify output type
|
||||
# Supported values are :
|
||||
# - svg (support animations and transparency)
|
||||
# - png (support transparency)
|
||||
# - jpeg
|
||||
config_output:
|
||||
description: Output image type
|
||||
default: svg
|
||||
|
||||
# Enable or disable SVG animations
|
||||
config_animations:
|
||||
description: Enable or disable SVG animations
|
||||
default: yes
|
||||
|
||||
# Number of repositories to use for metrics
|
||||
# A high number increase metrics accuracy, but will consume additional API requests when using plugins
|
||||
repositories:
|
||||
@@ -71,11 +85,17 @@ inputs:
|
||||
default: "header, activity, community, repositories, metadata"
|
||||
|
||||
# Google PageSpeed plugin
|
||||
# Enable it to compute the performance for the website attached to "user"
|
||||
# Enable it to compute the performance of provided website
|
||||
plugin_pagespeed:
|
||||
description: Enable Google PageSpeed metrics for user's website
|
||||
default: no
|
||||
|
||||
# Website to audit with PageSpeed
|
||||
# Leave empty to default to the website attached to "user"'s GitHub account
|
||||
plugin_pagespeed_url:
|
||||
description: Website to audit with PageSpeed
|
||||
default: ""
|
||||
|
||||
# Display additional PageSpeed audit metrics
|
||||
# The following are displayed :
|
||||
# First Contentful Paint, Speed Index, Largest Contentful Paint, Time to Interactive, Total Blocking Time, Cumulative Layout Shift
|
||||
@@ -217,6 +237,12 @@ inputs:
|
||||
description: Posts external source
|
||||
default: ""
|
||||
|
||||
# Posts source username
|
||||
# Leave empty to default to the login "user"'s GitHub account
|
||||
plugin_posts_user:
|
||||
description: Posts external source username
|
||||
default: ""
|
||||
|
||||
# Number of posts to display
|
||||
plugin_posts_limit:
|
||||
description: Number of posts to display
|
||||
@@ -297,6 +323,12 @@ inputs:
|
||||
description: Display recent tweets
|
||||
default: no
|
||||
|
||||
# Twitter username
|
||||
# Leave empty to default to the twitter account attached to "user"'s GitHub account
|
||||
plugin_tweets_user:
|
||||
description: Twitter username
|
||||
default: ""
|
||||
|
||||
# Tweets API token (required when tweets plugin is enabled)
|
||||
# See https://apps.twitter.com for more informations
|
||||
plugin_tweets_token:
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
//Load configuration
|
||||
const {conf, Plugins, Templates} = await setup({log:false})
|
||||
const {conf, Plugins, Templates} = await setup({log:false, nosettings:true})
|
||||
info("Setup", "complete")
|
||||
info("Version", conf.package.version)
|
||||
|
||||
@@ -110,9 +110,13 @@
|
||||
|
||||
//Config
|
||||
const config = {
|
||||
"config.timezone":input.string("config_timezone")
|
||||
"config.timezone":input.string("config_timezone"),
|
||||
"config.output":input.string("config_output"),
|
||||
"config.animations":input.bool("config_animations"),
|
||||
}
|
||||
info("Timezone", config["config.timezone"] ?? "(system default)")
|
||||
info("Convert SVG", config["config.output"] ?? "(no)")
|
||||
info("Enable SVG animations", config["config.animations"])
|
||||
|
||||
//Additional plugins
|
||||
const plugins = {
|
||||
@@ -137,6 +141,8 @@
|
||||
if (plugins.pagespeed.enabled) {
|
||||
plugins.pagespeed.token = input.string("plugin_pagespeed_token")
|
||||
info("Pagespeed token", plugins.pagespeed.token, {token:true})
|
||||
for (const option of ["url"])
|
||||
info(`Pagespeed ${option}`, q[`pagespeed.${option}`] = input.string(`plugin_pagespeed_${option}`))
|
||||
for (const option of ["detailed", "screenshot"])
|
||||
info(`Pagespeed ${option}`, q[`pagespeed.${option}`] = input.bool(`plugin_pagespeed_${option}`))
|
||||
}
|
||||
@@ -163,7 +169,7 @@
|
||||
}
|
||||
//Posts
|
||||
if (plugins.posts.enabled) {
|
||||
for (const option of ["source"])
|
||||
for (const option of ["source", "user"])
|
||||
info(`Posts ${option}`, q[`posts.${option}`] = input.string(`plugin_posts_${option}`))
|
||||
for (const option of ["limit"])
|
||||
info(`Posts ${option}`, q[`posts.${option}`] = input.number(`plugin_posts_${option}`))
|
||||
@@ -191,6 +197,8 @@
|
||||
if (plugins.tweets.enabled) {
|
||||
plugins.tweets.token = input.string("plugin_tweets_token")
|
||||
info("Tweets token", plugins.tweets.token, {token:true})
|
||||
for (const option of ["user"])
|
||||
info(`Tweets ${option}`, q[`tweets.${option}`] = input.string(`plugin_tweets_${option}`))
|
||||
for (const option of ["limit"])
|
||||
info(`Tweets ${option}`, q[`tweets.${option}`] = input.number(`plugin_tweets_${option}`))
|
||||
}
|
||||
@@ -209,7 +217,7 @@
|
||||
q = {...query, ...q, base:false, ...base, ...config, repositories, template}
|
||||
|
||||
//Render metrics
|
||||
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
|
||||
const {rendered} = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
|
||||
info("Rendering", "complete")
|
||||
|
||||
//Commit to repository
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import SVGO from "svgo"
|
||||
|
||||
//Setup
|
||||
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false}, {Plugins, Templates}) {
|
||||
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
|
||||
//Compute rendering
|
||||
try {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
throw new Error("unsupported template")
|
||||
const {image, style, fonts} = conf.templates[template]
|
||||
const queries = conf.queries
|
||||
const data = {base:{}, config:{}, errors:[], plugins:{}, computed:{}}
|
||||
const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
|
||||
|
||||
//Base parts
|
||||
{
|
||||
@@ -83,27 +83,32 @@
|
||||
//Template rendering
|
||||
console.debug(`metrics/compute/${login} > render`)
|
||||
let rendered = await ejs.render(image, {...data, s, style, fonts}, {async:true})
|
||||
//Apply resizing
|
||||
const {resized, mime} = await svgresize(rendered, {convert})
|
||||
rendered = resized
|
||||
|
||||
//Optimize rendering
|
||||
if ((conf.optimize)&&(!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
|
||||
const {data:optimized} = await svgo.optimize(rendered)
|
||||
rendered = optimized
|
||||
}
|
||||
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
const libxmljs = (await import("libxmljs")).default
|
||||
const parsed = libxmljs.parseXml(rendered)
|
||||
if (parsed.errors.length)
|
||||
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
|
||||
//Additional SVG transformations
|
||||
if (/svg/.test(mime)) {
|
||||
//Optimize rendering
|
||||
if ((conf.optimize)&&(!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
|
||||
const {data:optimized} = await svgo.optimize(rendered)
|
||||
rendered = optimized
|
||||
}
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
const libxmljs = (await import("libxmljs")).default
|
||||
const parsed = libxmljs.parseXml(rendered)
|
||||
if (parsed.errors.length)
|
||||
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
|
||||
}
|
||||
}
|
||||
|
||||
//Result
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
return rendered
|
||||
return {rendered, mime}
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
@@ -174,6 +179,45 @@
|
||||
})
|
||||
}
|
||||
|
||||
/** Render svg */
|
||||
async function svgresize(svg, {convert} = {}) {
|
||||
//Instantiate browser if needed
|
||||
if (!svgresize.browser) {
|
||||
svgresize.browser = await puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
|
||||
console.debug(`metrics/svgresize > started ${await svgresize.browser.version()}`)
|
||||
}
|
||||
//Render through browser and resize height
|
||||
const page = await svgresize.browser.newPage()
|
||||
await page.setContent(svg, {waitUntil:"load"})
|
||||
let mime = "image/svg+xml"
|
||||
let {resized, width, height} = await page.evaluate(async () => {
|
||||
//Disable animations
|
||||
const animated = !document.querySelector("svg").classList.contains("no-animations")
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.add("no-animations")
|
||||
//Get bounds and resize
|
||||
let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
|
||||
height = Math.ceil(height)
|
||||
width = Math.ceil(width)
|
||||
//Resize svg
|
||||
document.querySelector("svg").setAttribute("height", height)
|
||||
//Enable animations
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.remove("no-animations")
|
||||
//Result
|
||||
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
|
||||
})
|
||||
//Convert if required
|
||||
if (convert) {
|
||||
console.debug(`metrics/svgresize > convert to ${convert}`)
|
||||
resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true})
|
||||
mime = `image/${convert}`
|
||||
}
|
||||
//Result
|
||||
await page.close()
|
||||
return {resized, mime}
|
||||
}
|
||||
|
||||
/** Placeholder generator */
|
||||
function placeholder({data, conf, q}) {
|
||||
//Proxifier
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
const Plugins = {}
|
||||
|
||||
/** Setup */
|
||||
export default async function ({log = true} = {}) {
|
||||
export default async function ({log = true, nosettings = false} = {}) {
|
||||
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../..")
|
||||
@@ -33,8 +33,12 @@
|
||||
//Load settings
|
||||
logger(`metrics/setup > load settings.json`)
|
||||
if (fs.existsSync(__settings)) {
|
||||
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
|
||||
logger(`metrics/setup > load settings.json > success`)
|
||||
if (nosettings)
|
||||
logger(`metrics/setup > load settings.json > skipped because no settings is enabled`)
|
||||
else {
|
||||
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
|
||||
logger(`metrics/setup > load settings.json > success`)
|
||||
}
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > load settings.json > (missing)`)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import app from "./instance.mjs"
|
||||
|
||||
//Start app
|
||||
await app({mock:process.env.USE_MOCKED_DATA})
|
||||
await app({mock:process.env.USE_MOCKED_DATA, nosettings:process.env.NO_SETTINGS})
|
||||
@@ -11,11 +11,12 @@
|
||||
import metrics from "../metrics.mjs"
|
||||
|
||||
/** App */
|
||||
export default async function ({mock = false} = {}) {
|
||||
export default async function ({mock = false, nosettings = false} = {}) {
|
||||
|
||||
//Load configuration settings
|
||||
const {conf, Plugins, Templates} = await setup()
|
||||
const {conf, Plugins, Templates} = await setup({nosettings})
|
||||
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
|
||||
cache.placeholder = new Map()
|
||||
|
||||
//Apply configuration mocking if needed
|
||||
if (mock) {
|
||||
@@ -66,8 +67,8 @@
|
||||
|
||||
//Base routes
|
||||
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 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 enabled = Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)
|
||||
const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
|
||||
const enabled = Object.entries(Plugins).map(([name]) => ({name, enabled:plugins[name].enabled ?? false}))
|
||||
const actions = {flush:new Map()}
|
||||
app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||
@@ -117,11 +118,20 @@
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
//Read cached data if possible
|
||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.send(cache.get(login))
|
||||
return
|
||||
}
|
||||
//Placeholder
|
||||
if ((login === "placeholder")&&(cache.placeholder.has(Object.keys(req.query).sort().join("-")))) {
|
||||
const {rendered, mime} = cache.placeholder.get(Object.keys(req.query).sort().join("-"))
|
||||
res.header("Content-Type", mime)
|
||||
res.send(rendered)
|
||||
return
|
||||
}
|
||||
//User cached
|
||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||
const {rendered, mime} = cache.get(login)
|
||||
res.header("Content-Type", mime)
|
||||
res.send(rendered)
|
||||
return
|
||||
}
|
||||
//Maximum simultaneous users
|
||||
if ((maxusers)&&(cache.size()+1 > maxusers)) {
|
||||
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
||||
@@ -133,12 +143,19 @@
|
||||
//Render
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`)
|
||||
const q = parse(req.query)
|
||||
const rendered = await metrics({login, q}, {graphql, rest, plugins, conf, die:q["plugins.errors.fatal"] ?? false, verify:q["verify"] ?? false}, {Plugins, Templates})
|
||||
const {rendered, mime} = await metrics({login, q}, {
|
||||
graphql, rest, plugins, conf,
|
||||
die:q["plugins.errors.fatal"] ?? false,
|
||||
verify:q["verify"] ?? false,
|
||||
convert:["jpeg", "png"].includes(q["config.output"]) ? q["config.output"] : null
|
||||
}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)&&(login !== "placeholder"))
|
||||
cache.put(login, rendered, cached)
|
||||
if (login === "placeholder")
|
||||
cache.placeholder.set(Object.keys(req.query).sort().join("-"), rendered)
|
||||
if ((!debug)&&(cached))
|
||||
cache.put(login, {rendered, mime}, cached)
|
||||
//Send response
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.header("Content-Type", mime)
|
||||
res.send(rendered)
|
||||
}
|
||||
//Internal error
|
||||
@@ -167,7 +184,7 @@
|
||||
`Cached time │ ${cached} seconds`,
|
||||
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
|
||||
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
|
||||
`Plugins enabled │ ${enabled.join(", ")}`,
|
||||
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
|
||||
`Server ready !`
|
||||
].join("\n")))
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
templates:{
|
||||
list:templates,
|
||||
selected:url.get("template") || templates[0],
|
||||
selected:url.get("template") || templates[0].name,
|
||||
loaded:{},
|
||||
placeholder:"",
|
||||
descriptions:{
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
<div class="step">
|
||||
<h2>2. Select a template</h2>
|
||||
<div class="templates">
|
||||
<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 v-for="template in templates.list" :key="template" v-show="templates.descriptions[template.name] !== '(hidden)'" :class="{'not-available':!template.enabled}" :title="!template.enabled ? 'This template is not enabled on web instance, use it with GitHub actions !' : ''">
|
||||
<input type="radio" v-model="templates.selected" :value="template.name" @change="load" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template.name] || template.name }}
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="plugins.base.length">
|
||||
@@ -45,12 +45,11 @@
|
||||
<template v-if="plugins.list.length">
|
||||
<h3>2.2 Enable additional plugins</h3>
|
||||
<div class="plugins">
|
||||
<label v-for="plugin in plugins.list" :key="plugin">
|
||||
<input type="checkbox" v-model="plugins.enabled[plugin]" @change="load" :disabled="generated.pending">
|
||||
{{ plugins.descriptions[plugin] || plugin }}
|
||||
<label v-for="plugin in plugins.list" :key="plugin" :class="{'not-available':!plugin.enabled}" :title="!plugin.enabled ? 'This plugin is not enabled on web instance, use it with GitHub actions !' : ''">
|
||||
<input type="checkbox" v-model="plugins.enabled[plugin.name]" @change="load" :disabled="(!plugin.enabled)||(generated.pending)">
|
||||
{{ plugins.descriptions[plugin.name] || plugin.name }}
|
||||
</label>
|
||||
</div>
|
||||
<i>*Additional plugins may be available when used as GitHub Action</i>
|
||||
<template v-if="(plugins.enabled.tweets)||(plugins.enabled.music)||(plugins.enabled.pagespeed)||(plugins.enabled.languages)||(plugins.enabled.habits)||(plugins.enabled.posts)||(plugins.enabled.isocalendar)||(plugins.enabled.projects)||(plugins.enabled.topics)">
|
||||
<h3>2.3 Configure additional plugins</h3>
|
||||
<div class="options">
|
||||
@@ -58,59 +57,59 @@
|
||||
<h4>{{ plugins.descriptions.tweets }}</h4>
|
||||
<label>
|
||||
Number of tweets to display
|
||||
<input type="number" v-model="plugins.options['tweets.limit']" min="1" max="10" @change="load">
|
||||
<input type="number" v-model="plugins.options['tweets.limit']" min="1" max="10" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.music">
|
||||
<h4>{{ plugins.descriptions.music }}</h4>
|
||||
<label>
|
||||
Playlist embed link
|
||||
<input type="text" v-model="plugins.options['music.playlist']" placeholder="https://embed.music.apple.com/en/playlist/">
|
||||
<input type="text" v-model="plugins.options['music.playlist']" placeholder="https://embed.music.apple.com/en/playlist/" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Number of tracks to display
|
||||
<input type="number" v-model="plugins.options['music.limit']" min="1" @change="load">
|
||||
<input type="number" v-model="plugins.options['music.limit']" min="1" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.pagespeed">
|
||||
<h4>{{ plugins.descriptions.pagespeed }}</h4>
|
||||
<label>
|
||||
Detailed PageSpeed report
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load">
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Include a website screenshot
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load">
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.languages">
|
||||
<h4>{{ plugins.descriptions.languages }}</h4>
|
||||
<label>
|
||||
Ignored languages (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.ignored']" @change="load">
|
||||
<input type="text" v-model="plugins.options['languages.ignored']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Skipped repositories (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.skipped']" @change="load">
|
||||
<input type="text" v-model="plugins.options['languages.skipped']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.habits">
|
||||
<h4>{{ plugins.descriptions.habits }}</h4>
|
||||
<label>
|
||||
Number of events for habits
|
||||
<input type="number" v-model="plugins.options['habits.from']" min="1" max="1000">
|
||||
<input type="number" v-model="plugins.options['habits.from']" min="1" max="1000" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Number of days for habits
|
||||
<input type="number" v-model="plugins.options['habits.days']" min="1" max="30">
|
||||
<input type="number" v-model="plugins.options['habits.days']" min="1" max="30" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Display tidbits
|
||||
<input type="checkbox" v-model="plugins.options['habits.facts']" @change="load">
|
||||
<input type="checkbox" v-model="plugins.options['habits.facts']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Display activity charts
|
||||
<input type="checkbox" v-model="plugins.options['habits.charts']" @change="load">
|
||||
<input type="checkbox" v-model="plugins.options['habits.charts']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.posts">
|
||||
@@ -123,14 +122,14 @@
|
||||
</label>
|
||||
<label>
|
||||
Number of posts to display
|
||||
<input type="number" v-model="plugins.options['posts.limit']" min="1" @change="load">
|
||||
<input type="number" v-model="plugins.options['posts.limit']" min="1" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.isocalendar">
|
||||
<h4>{{ plugins.descriptions.isocalendar }}</h4>
|
||||
<label>
|
||||
Isocalendar duration
|
||||
<select v-model="plugins.options['isocalendar.duration']">
|
||||
<select v-model="plugins.options['isocalendar.duration']" :disabled="generated.pending">
|
||||
<option value="half-year">Half year</option>
|
||||
<option value="full-year">Full year</option>
|
||||
</select>
|
||||
@@ -140,14 +139,14 @@
|
||||
<h4>{{ plugins.descriptions.topics }}</h4>
|
||||
<label>
|
||||
Topics display mode
|
||||
<select v-model="plugins.options['topics.mode']" @change="load">
|
||||
<select v-model="plugins.options['topics.mode']" @change="load" :disabled="generated.pending">
|
||||
<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']">
|
||||
<select v-model="plugins.options['topics.sort']" :disabled="generated.pending">
|
||||
<option value="starred">Recently starred by you</option>
|
||||
<option value="stars">Most stars</option>
|
||||
<option value="activity">Recent actity</option>
|
||||
@@ -156,18 +155,18 @@
|
||||
</label>
|
||||
<label>
|
||||
Number of topics to display
|
||||
<input type="number" v-model="plugins.options['topics.limit']" @change="load">
|
||||
<input type="number" v-model="plugins.options['topics.limit']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.projects">
|
||||
<h4>{{ plugins.descriptions.projects }}</h4>
|
||||
<label>
|
||||
Number of projects to display
|
||||
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load">
|
||||
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Repositories projects to display (comma separated)
|
||||
<input type="text" v-model="plugins.options['projects.repositories']" @change="load">
|
||||
<input type="text" v-model="plugins.options['projects.repositories']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
transition: background-color .4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.not-available {
|
||||
opacity: .3;
|
||||
}
|
||||
/* Generator */
|
||||
.generator {
|
||||
display: flex;
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
if ((!enabled)||(!q.pagespeed)||(!data.user.websiteUrl))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false} = q
|
||||
let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false, "pagespeed.url":url = data.user.websiteUrl} = q
|
||||
//Duration in days
|
||||
detailed = !!detailed
|
||||
//Format url if needed
|
||||
let url = data.user.websiteUrl
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const result = {url, detailed, scores:[], metrics:{}}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
//Setup
|
||||
export default async function ({imports, data, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, imports, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.posts))
|
||||
return null
|
||||
//Parameters override
|
||||
const login = data.user.login
|
||||
let {"posts.source":source = "", "posts.limit":limit = 4} = q
|
||||
let {"posts.source":source = "", "posts.limit":limit = 4, "posts.user":user = login} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(30, Number(limit)))
|
||||
//Retrieve posts
|
||||
@@ -17,7 +16,7 @@
|
||||
//Dev.to
|
||||
case "dev.to":{
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${login}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date}))
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${user}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date}))
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
if ((!enabled)||(!q.tweets))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"tweets.limit":limit = 2} = q
|
||||
let {"tweets.limit":limit = 2, "tweets.user":username = data.user.twitterUsername} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(10, Number(limit)))
|
||||
//Load user profile
|
||||
const username = data.user.twitterUsername
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
|
||||
const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
//Load tweets
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 12
|
||||
+ (!!base.header)*80 + (user.isHireable)*16
|
||||
+ (!!base.metadata)*38
|
||||
+ ((!!base.activity)||(!!base.community))*128
|
||||
+ (!!base.repositories)*108
|
||||
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
|
||||
+ (!!plugins.followup)*68
|
||||
+ (!!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)
|
||||
+ (!!plugins.posts)*64 + (plugins.posts?.list?.length ?? 0)*40
|
||||
+ (!!plugins.isocalendar)*192 + (plugins.isocalendar?.duration === 'full-year')*100
|
||||
+ (!!plugins.gists)*68
|
||||
+ (!!plugins.topics)*160
|
||||
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
|
||||
+ (!!plugins.tweets)*64 + (plugins.tweets?.list?.length ?? 0)*90
|
||||
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!plugins.habits))+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.music)+(!!plugins.posts)+(!!plugins.isocalendar)+(!!plugins.gists)+(!!plugins.topics)+(!!plugins.projects))-1))*4
|
||||
%>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="99999" class="<%= !animated ? 'no-animations' : '' %>">
|
||||
|
||||
<defs><style><%= fonts %></style></defs>
|
||||
|
||||
@@ -802,6 +784,7 @@
|
||||
</footer>
|
||||
<% } %>
|
||||
|
||||
<div id="metrics-end"></div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 74 KiB |
@@ -421,4 +421,18 @@
|
||||
--color-calendar-graph-day-L3-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L2-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-border: rgba(27,31,35,0.06);
|
||||
}
|
||||
|
||||
/* End delimiter */
|
||||
#metrics-end {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-animations * {
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
animation-delay: -0.0001s !important;
|
||||
animation-duration: 0s !important;
|
||||
animation-play-state: paused !important;
|
||||
caret-color: transparent !important;
|
||||
}
|
||||
@@ -18,6 +18,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
//Animations
|
||||
if ("config.animations" in q) {
|
||||
data.animated = q["config.animations"]
|
||||
console.debug(`metrics/compute/${login} > animations ${data.animated ? "enabled" : "disabled"}`)
|
||||
}
|
||||
|
||||
//Plugins
|
||||
for (const name of Object.keys(imports.plugins)) {
|
||||
if (!plugins[name].enabled)
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
<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
|
||||
+ Math.max(0, ((!!base.header)+(!!base.metadata)+(!!plugins.followup)+(!!plugins.projects))-1)*4
|
||||
%>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="99999" class="<%= !animated ? 'no-animations' : '' %>">
|
||||
|
||||
<defs><style><%= fonts %></style></defs>
|
||||
|
||||
@@ -216,6 +209,7 @@
|
||||
</footer>
|
||||
<% } %>
|
||||
|
||||
<div id="metrics-end"></div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,17 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 48
|
||||
+ (!!base.header)*62
|
||||
+ (!!base.metadata)*108
|
||||
+ (!!base.activity)*108
|
||||
+ (!!base.community)*94
|
||||
+ (!!base.repositories)*142
|
||||
+ ((!!base.repositories)*(!!plugins.traffic))*18
|
||||
+ ((!!base.repositories)*(!!plugins.followup))*102
|
||||
+ ((!!base.repositories)*(!!plugins.lines))*34
|
||||
+ (!!plugins.pagespeed)*110 + (plugins.pagespeed?.detailed ?? 0)*6*16
|
||||
+ (!!plugins.languages)*124
|
||||
+ (!!plugins.gists)*58
|
||||
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.gists))-1))*20
|
||||
%>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="99999" class="<%= !animated ? 'no-animations' : '' %>">
|
||||
<%
|
||||
meta.$ = `<span class="ps1-path">${`${user.login}`.toLocaleLowerCase()}@metrics</span>:<span class="ps1-location">~</span>${computed.token.scopes.includes("repo") ? "#" : "$"}`
|
||||
meta.animations = !meta.placeholder ? {stdin:.16, stdout:.28, length:(2+Object.keys(base).length+Object.keys(plugins).length)} : {stdin:0, stdout:0, length:0}
|
||||
@@ -174,6 +161,7 @@ Total <%= plugins.gists.totalCount %> gist<%= s(plugins.gists.totalCount) %>
|
||||
<footer>Connection reset by <%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %></footer><%# -%>
|
||||
<% } -%></pre>
|
||||
|
||||
<div id="metrics-end"></div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -56,6 +56,7 @@
|
||||
}
|
||||
pre {
|
||||
background: #42092B;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -124,4 +125,18 @@
|
||||
--color-calendar-graph-day-L3-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L2-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-border: rgba(27,31,35,0.06);
|
||||
}
|
||||
|
||||
/* End delimiter */
|
||||
#metrics-end {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-animations * {
|
||||
transition-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
animation-delay: -0.0001s !important;
|
||||
animation-duration: 0s !important;
|
||||
animation-play-state: paused !important;
|
||||
caret-color: transparent !important;
|
||||
}
|
||||
@@ -31,37 +31,66 @@
|
||||
//Web instance
|
||||
const web = {}
|
||||
web.run = async (vars) => (await axios(`http://localhost:3000/lowlighter?${new url.URLSearchParams(Object.fromEntries(Object.entries(vars).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value])))}`)).status === 200
|
||||
beforeAll(async () => await new Promise((solve, reject) => {
|
||||
let stdout = ""
|
||||
web.instance = processes.spawn("node", ["source/app/web/index.mjs"], {env:{...process.env, USE_MOCKED_DATA:true}})
|
||||
web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null))
|
||||
web.instance.stderr.on("data", data => console.error(`${data}`))
|
||||
}))
|
||||
afterAll(async () => await web.instance.kill())
|
||||
beforeAll(async done => {
|
||||
await new Promise((solve, reject) => {
|
||||
let stdout = ""
|
||||
web.instance = processes.spawn("node", ["source/app/web/index.mjs"], {env:{...process.env, USE_MOCKED_DATA:true, NO_SETTINGS:true}})
|
||||
web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null))
|
||||
web.instance.stderr.on("data", data => console.error(`${data}`))
|
||||
})
|
||||
done()
|
||||
})
|
||||
afterAll(async done => {
|
||||
await web.instance.kill("SIGKILL")
|
||||
done()
|
||||
})
|
||||
|
||||
//Test cases
|
||||
const tests = [
|
||||
["Base (header)", {
|
||||
base:"header"
|
||||
base:"header",
|
||||
base_header:true,
|
||||
}],
|
||||
["Base (activity", {
|
||||
base:"activity"
|
||||
base:"activity",
|
||||
base_activity:true,
|
||||
}],
|
||||
["Base (community)", {
|
||||
base:"community"
|
||||
base:"community",
|
||||
base_community:true,
|
||||
}],
|
||||
["Base (repositories)", {
|
||||
base:"repositories"
|
||||
base:"repositories",
|
||||
base_repositories:true,
|
||||
}],
|
||||
["Base (metadata)", {
|
||||
base:"metadata"
|
||||
base:"metadata",
|
||||
base_metadata:true,
|
||||
}],
|
||||
["Base (complete)", {
|
||||
base:"header, activity, community, repositories, metadata"
|
||||
base:"header, activity, community, repositories, metadata",
|
||||
base_header:true,
|
||||
base_activity:true,
|
||||
base_community:true,
|
||||
base_repositories:true,
|
||||
base_metadata:true,
|
||||
}],
|
||||
["Image output (jpeg)", {
|
||||
config_output:"jpeg",
|
||||
}],
|
||||
["Image output (png)", {
|
||||
config_output:"png",
|
||||
}],
|
||||
["Disable animations", {
|
||||
config_animations:"no",
|
||||
}],
|
||||
["PageSpeed plugin (default)", {
|
||||
plugin_pagespeed:true,
|
||||
}, {skip:["repository"]}],
|
||||
["PageSpeed plugin (different url)", {
|
||||
plugin_pagespeed:true,
|
||||
plugin_pagespeed_url:"github.com",
|
||||
}, {skip:["repository"]}],
|
||||
["PageSpeed plugin (detailed)", {
|
||||
plugin_pagespeed:true,
|
||||
plugin_pagespeed_detailed:true,
|
||||
@@ -179,6 +208,10 @@
|
||||
["Tweets plugin (default)", {
|
||||
plugin_tweets:true,
|
||||
}, {skip:["terminal", "repository"]}],
|
||||
["Tweets plugin (different user)", {
|
||||
plugin_tweets:true,
|
||||
plugin_tweets_user:"twitterdev",
|
||||
}, {skip:["terminal", "repository"]}],
|
||||
["Posts plugin (dev.to)", {
|
||||
user:"lowlighter",
|
||||
plugin_posts:true,
|
||||
|
||||
Reference in New Issue
Block a user