refactor(app/web): new features (#1124) [skip ci]
2
.github/config/label.yml
vendored
@@ -2,7 +2,7 @@
|
||||
- source/app/action/**
|
||||
- source/app/web/**
|
||||
✨ metrics insights:
|
||||
- source/app/web/statics/about/**
|
||||
- source/app/web/statics/insights/**
|
||||
|
||||
🧩 plugins:
|
||||
- source/plugins/**
|
||||
|
||||
@@ -79,8 +79,8 @@ Generate metrics that can be embedded everywhere, including your GitHub profile
|
||||
<th colspan="2"><h2>🦑 Try it now!</h2></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><a href="https://metrics.lecoq.io">📊 Metrics embed</a></th>
|
||||
<th><a href="https://metrics.lecoq.io/about">✨ Metrics insights</a></th>
|
||||
<th><a href="https://metrics.lecoq.io/embed">📊 Metrics embed</a></th>
|
||||
<th><a href="https://metrics.lecoq.io/insights">✨ Metrics insights</a></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
||||
@@ -220,7 +220,8 @@ export default async function(
|
||||
},
|
||||
//Settings and tokens
|
||||
{
|
||||
enabled = false
|
||||
enabled = false,
|
||||
extras = false,
|
||||
} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
@@ -241,7 +242,7 @@ export default async function(
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
17
.github/scripts/preview.mjs
vendored
@@ -9,14 +9,12 @@ const __metrics = paths.join(paths.dirname(url.fileURLToPath(import.meta.url)),
|
||||
const __templates = paths.join(paths.join(__metrics, "source/templates/"))
|
||||
const __node_modules = paths.join(paths.join(__metrics, "node_modules"))
|
||||
const __web = paths.join(paths.join(__metrics, "source/app/web/statics"))
|
||||
const __web_about = paths.join(paths.join(__web, "about"))
|
||||
|
||||
const __preview = paths.join(paths.join(__web, "preview"))
|
||||
const __preview_js = paths.join(__preview, ".js")
|
||||
const __preview_css = paths.join(__preview, ".css")
|
||||
const __preview_templates = paths.join(__preview, ".templates")
|
||||
const __preview_templates_ = paths.join(__preview, ".templates_")
|
||||
const __preview_about = paths.join(__preview, "about/.statics")
|
||||
|
||||
//Extract from web server
|
||||
const {conf, Templates} = await setup({log: false})
|
||||
@@ -34,7 +32,6 @@ await fs.mkdir(__preview_js, {recursive: true})
|
||||
await fs.mkdir(__preview_css, {recursive: true})
|
||||
await fs.mkdir(__preview_templates, {recursive: true})
|
||||
await fs.mkdir(__preview_templates_, {recursive: true})
|
||||
await fs.mkdir(__preview_about, {recursive: true})
|
||||
|
||||
//Web
|
||||
fs.copyFile(paths.join(__web, "index.html"), paths.join(__preview, "index.html"))
|
||||
@@ -81,9 +78,15 @@ fs.copyFile(paths.join(__node_modules, "clipboard/dist/clipboard.min.js"), paths
|
||||
//Meta
|
||||
fs.writeFile(paths.join(__preview, ".version"), JSON.stringify(`${conf.package.version}-preview`))
|
||||
fs.writeFile(paths.join(__preview, ".hosted"), JSON.stringify({by: "metrics", link: "https://github.com/lowlighter/metrics"}))
|
||||
//About
|
||||
fs.copyFile(paths.join(__web, "about", "index.html"), paths.join(__preview, "about", "index.html"))
|
||||
for (const file of await fs.readdir(__web_about)) {
|
||||
//Insights
|
||||
for (const insight of ["insights", "about"]) {
|
||||
const __web_insights = paths.join(paths.join(__web, insight))
|
||||
const __preview_insights = paths.join(__preview, `${insight}/.statics`)
|
||||
await fs.mkdir(__preview_insights, {recursive: true})
|
||||
|
||||
fs.copyFile(paths.join(__web, insight, "index.html"), paths.join(__preview, insight, "index.html"))
|
||||
for (const file of await fs.readdir(__web_insights)) {
|
||||
if (file !== ".statics")
|
||||
fs.copyFile(paths.join(__web_about, file), paths.join(__preview_about, file))
|
||||
fs.copyFile(paths.join(__web_insights, file), paths.join(__preview_insights, file))
|
||||
}
|
||||
}
|
||||
6
.github/scripts/quickstart/plugin/index.mjs
vendored
@@ -1,15 +1,15 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, computed, rest, graphql, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, computed, rest, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.<%= name %>))
|
||||
if ((!enabled) || (!q.<%= name %>) || (!imports.metadata.plugins.<%= name %>.extras("enabled", {extras})))
|
||||
return null
|
||||
//Results
|
||||
return {}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
||||
//Initialization
|
||||
const pending = []
|
||||
const {queries} = conf
|
||||
const extras = {css: (conf.settings.extras?.css ?? conf.settings.extras?.default) ? q["extras.css"] ?? "" : "", js: (conf.settings.extras?.js ?? conf.settings.extras?.default) ? q["extras.js"] ?? "" : ""}
|
||||
const extras = {css: imports.metadata.plugins.core.extras("extras_css", {...conf.settings, error:false}) ? q["extras.css"] ?? "" : "", js: imports.metadata.plugins.core.extras("extras_js", {...conf.settings, error:false}) ? q["extras.js"] ?? "" : ""}
|
||||
const data = {q, animated: true, large: false, base: {}, config: {}, errors: [], plugins: {}, computed: {}, extras, postscripts: []}
|
||||
const imports = {
|
||||
plugins: Plugins,
|
||||
@@ -184,7 +184,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
|
||||
if ((conf.settings?.optimize === true) || (conf.settings?.optimize?.includes?.("svg")))
|
||||
rendered = await imports.svg.optimize.svg(rendered, q, experimental)
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
if ((verify)&&(imports.metadata.plugins.core.extras("verify", {...conf.settings, error:false}))) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
let libxmljs = null
|
||||
try {
|
||||
@@ -281,9 +281,9 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
|
||||
console.debug(`metrics/compute/${login} > insights > generating data`)
|
||||
const result = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})
|
||||
const json = JSON.stringify(result)
|
||||
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
|
||||
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
|
||||
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
|
||||
await page.goto(`${server}/insights/${login}?embed=1&localstorage=1`)
|
||||
await page.waitForSelector(".container .user", {timeout: 10 * 60 * 1000})
|
||||
|
||||
//Rendering
|
||||
@@ -297,7 +297,7 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
|
||||
</head>
|
||||
<body>
|
||||
${await page.evaluate(() => document.querySelector("main").outerHTML)}
|
||||
${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
|
||||
${(await Promise.all([".css/style.vars.css", ".css/style.css", "insights/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
|
||||
</body>
|
||||
</html>`
|
||||
await browser.close()
|
||||
|
||||
@@ -111,7 +111,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if (!meta.supports?.includes(context))
|
||||
throw {error: {message: `Not supported for: ${context}`, instance: new Error()}}
|
||||
throw {error: {message: `Unsupported context ${context}`, instance: new Error()}}
|
||||
}
|
||||
//Special values replacer
|
||||
const replacer = value => {
|
||||
@@ -214,34 +214,60 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
|
||||
|
||||
//Extra features parser
|
||||
{
|
||||
meta.extras = function(input, {extras = {}}) {
|
||||
meta.extras = function(input, {extras = {}, error = true}) {
|
||||
const key = metadata.to.yaml(input, {name})
|
||||
try {
|
||||
//Required permissions
|
||||
const required = inputs[metadata.to.yaml(input, {name})]?.extras ?? null
|
||||
const required = inputs[key]?.extras ?? null
|
||||
if (!required)
|
||||
return true
|
||||
console.debug(`metrics/extras > ${name} > ${input} > require [${required}]`)
|
||||
console.debug(`metrics/extras > ${name} > ${key} > require [${required}]`)
|
||||
|
||||
//Legacy handling
|
||||
const enabled = extras?.features ?? extras?.default ?? false
|
||||
if (typeof enabled === "boolean") {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > extras features is set to ${enabled}`)
|
||||
console.debug(`metrics/extras > ${name} > ${key} > extras features is set to ${enabled}`)
|
||||
if (!enabled)
|
||||
throw new Error()
|
||||
return enabled
|
||||
}
|
||||
if (!Array.isArray(required)) {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > extras is not a permission array, skipping`)
|
||||
console.debug(`metrics/extras > ${name} > ${key} > extras is not a permission array, skipping`)
|
||||
return false
|
||||
}
|
||||
|
||||
//Check permissions
|
||||
//Legacy options handling
|
||||
if (!Array.isArray(extras.features))
|
||||
throw new Error(`metrics/extras > ${name} > ${input} > extras.features is not an array`)
|
||||
throw new Error(`metrics/extras > ${name} > ${key} > extras.features is not an array`)
|
||||
if (extras.css) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.css is deprecated, use extras.features with "metrics.run.puppeteer.user.css" instead`)
|
||||
extras.features.push("metrics.run.puppeteer.user.css")
|
||||
}
|
||||
if (extras.js) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.js is deprecated, use extras.features with "metrics.run.puppeteer.user.js" instead`)
|
||||
extras.features.push("metrics.run.puppeteer.user.js")
|
||||
}
|
||||
if (extras.presets) {
|
||||
console.warn(`metrics/extras > ${name} > ${key} > extras.presets is deprecated, use extras.features with "metrics.setup.community.presets" instead`)
|
||||
extras.features.push("metrics.setup.community.presets")
|
||||
}
|
||||
|
||||
//Check permissions
|
||||
const missing = required.filter(permission => !extras.features.includes(permission))
|
||||
if (missing.length > 0) {
|
||||
console.debug(`metrics/extras > ${name} > ${input} > missing permissions [${missing}], skipping`)
|
||||
return false
|
||||
console.debug(`metrics/extras > ${name} > ${key} > missing permissions [${missing}]`)
|
||||
throw new Error()
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
if (!error) {
|
||||
console.debug(`metrics/extras > ${name} > ${key} > skipping (no error mode)`)
|
||||
return false
|
||||
}
|
||||
throw Object.assign(new Error(`Unsupported option "${key}"`), {extras:true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Action metadata
|
||||
@@ -576,7 +602,9 @@ metadata.to = {
|
||||
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
|
||||
},
|
||||
yaml(key, {name = ""} = {}) {
|
||||
const parts = [key.replaceAll(".", "_")]
|
||||
const parts = []
|
||||
if (key !== "enabled")
|
||||
parts.unshift(key.replaceAll(".", "_"))
|
||||
if (name)
|
||||
parts.unshift((name === "base") ? name : `plugin_${name}`)
|
||||
return parts.join("_")
|
||||
|
||||
@@ -78,6 +78,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
|
||||
logger("metrics/setup > load package.json > success")
|
||||
|
||||
//Load community templates
|
||||
if ((conf.settings.extras?.features?.includes("metrics.setup.community.templates"))||(conf.settings.extras?.features === true)||(conf.settings.extras?.default)) {
|
||||
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
|
||||
logger("metrics/setup > parsing community templates list")
|
||||
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
|
||||
@@ -136,6 +137,10 @@ export default async function({log = true, sandbox = false, community = {}} = {}
|
||||
else {
|
||||
logger("metrics/setup > no community templates to install")
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger("metrics/setup > community templates are disabled")
|
||||
}
|
||||
|
||||
//Load templates
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
@@ -188,6 +193,18 @@ export default async function({log = true, sandbox = false, community = {}} = {}
|
||||
//Load metadata
|
||||
conf.metadata = await metadata({log})
|
||||
|
||||
//Modes
|
||||
if ((!conf.settings.modes)||(!conf.settings.modes.length))
|
||||
conf.settings.modes = ["embed", "insights"]
|
||||
logger(`metrics/setup > setup > enabled modes ${JSON.stringify(conf.settings.modes)}`)
|
||||
|
||||
//Allowed outputs formats
|
||||
if ((!conf.settings.outputs)||(!conf.settings.outputs.length))
|
||||
conf.settings.outputs = metadata.inputs.config_output.values
|
||||
else
|
||||
conf.settings.outputs = conf.settings.outputs.filter(format => metadata.inputs.config_output.values.includes(format))
|
||||
logger(`metrics/setup > setup > allowed outputs ${JSON.stringify(conf.settings.outputs)}`)
|
||||
|
||||
//Store authenticated user
|
||||
if (conf.settings.token) {
|
||||
try {
|
||||
|
||||
@@ -125,6 +125,49 @@ export function formatters({timeZone} = {}) {
|
||||
return license.nickname ?? license.spdxId ?? license.name
|
||||
}
|
||||
|
||||
/**Error formatter */
|
||||
format.error = function(error, {descriptions = {}, ...attributes} = {}) {
|
||||
try {
|
||||
//Extras features error
|
||||
if (error.extras)
|
||||
throw {error: {message: error.message, instance: error}}
|
||||
//Already formatted error
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
//Custom description
|
||||
let message = "Unexpected error"
|
||||
if (descriptions.custom) {
|
||||
const description = descriptions.custom(error)
|
||||
if (description)
|
||||
message += ` (${description})`
|
||||
}
|
||||
//Axios error
|
||||
if (error.isAxiosError) {
|
||||
//Error code
|
||||
const status = error.response?.status
|
||||
message = `API error: ${status}`
|
||||
|
||||
//Error description (optional)
|
||||
if ((descriptions)&&(descriptions[status]))
|
||||
message += ` (${descriptions[status]})`
|
||||
else {
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? error.response.data?.error_description ?? error.response?.data?.message ?? null
|
||||
if (description)
|
||||
message += ` (${description})`
|
||||
}
|
||||
|
||||
//Error data
|
||||
console.debug(error.response.data)
|
||||
error = error.response?.data ?? null
|
||||
throw {error: {message, instance: error}}
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
}
|
||||
catch (error) {
|
||||
return Object.assign(error, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
return {format}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,15 +130,12 @@ export default async function({sandbox = false} = {}) {
|
||||
app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404))
|
||||
for (const template in conf.templates)
|
||||
app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`))
|
||||
//Placeholders
|
||||
app.use("/.placeholders", express.static(`${conf.paths.statics}/placeholders`))
|
||||
//Styles
|
||||
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`))
|
||||
app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`))
|
||||
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`))
|
||||
//Scripts
|
||||
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`))
|
||||
app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`))
|
||||
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`))
|
||||
app.get("/.js/faker.min.js", limiter, (req, res) => res.set({"Content-Type": "text/javascript"}).send("import {faker} from '/.js/faker/index.mjs';globalThis.faker=faker;globalThis.placeholder.init(globalThis)"))
|
||||
app.use("/.js/faker", express.static(`${conf.paths.node_modules}/@faker-js/faker/dist/esm`))
|
||||
@@ -176,12 +173,18 @@ export default async function({sandbox = false} = {}) {
|
||||
//Pending requests
|
||||
const pending = new Map()
|
||||
|
||||
//About routes
|
||||
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
|
||||
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/query/:login/:plugin/", async (req, res) => {
|
||||
//Metrics insights
|
||||
if (conf.settings.modes.includes("insights")) {
|
||||
console.debug("metrics/app > setup insights mode")
|
||||
//Legacy routes
|
||||
app.get("/about/*", (req, res) => res.redirect(req.path.replace("/about/", "/insights/")))
|
||||
//Static routes
|
||||
app.get("/insights/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/insights/index.html`))
|
||||
app.get("/insights/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/insights/index.html`))
|
||||
app.use("/insights/.statics/", express.static(`${conf.paths.statics}/insights`))
|
||||
//App routes
|
||||
app.get("/insights/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/insights/index.html`))
|
||||
app.get("/insights/query/:login/:plugin/", async (req, res) => {
|
||||
//Check username
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
@@ -194,11 +197,11 @@ export default async function({sandbox = false} = {}) {
|
||||
console.debug(`metrics/app/${login}/insights > 400 (invalid plugin name)`)
|
||||
return res.status(400).send("Bad request: plugin name seems invalid")
|
||||
}
|
||||
if (cache.get(`about.${login}.${plugin}`))
|
||||
return res.send(cache.get(`about.${login}.${plugin}`))
|
||||
if (cache.get(`insights.${login}.${plugin}`))
|
||||
return res.send(cache.get(`insights.${login}.${plugin}`))
|
||||
return res.status(204).send("No content: no data fetched yet")
|
||||
})
|
||||
app.get("/about/query/:login/", ...middlewares, async (req, res) => {
|
||||
app.get("/insights/query/:login/", ...middlewares, async (req, res) => {
|
||||
//Check username
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
@@ -209,34 +212,34 @@ export default async function({sandbox = false} = {}) {
|
||||
let solve = null
|
||||
try {
|
||||
//Prevent multiples requests
|
||||
if ((!debug) && (!mock) && (pending.has(`about.${login}`))) {
|
||||
if ((!debug) && (!mock) && (pending.has(`insights.${login}`))) {
|
||||
console.debug(`metrics/app/${login}/insights > awaiting pending request`)
|
||||
await pending.get(`about.${login}`)
|
||||
await pending.get(`insights.${login}`)
|
||||
}
|
||||
else {
|
||||
pending.set(`about.${login}`, new Promise(_solve => solve = _solve))
|
||||
pending.set(`insights.${login}`, new Promise(_solve => solve = _solve))
|
||||
}
|
||||
//Read cached data if possible
|
||||
if ((!debug) && (cached) && (cache.get(`about.${login}`))) {
|
||||
if ((!debug) && (cached) && (cache.get(`insights.${login}`))) {
|
||||
console.debug(`metrics/app/${login}/insights > using cached results`)
|
||||
return res.send(cache.get(`about.${login}`))
|
||||
return res.send(cache.get(`insights.${login}`))
|
||||
}
|
||||
//Compute metrics
|
||||
console.debug(`metrics/app/${login}/insights > compute insights`)
|
||||
const callbacks = {
|
||||
async plugin(login, plugin, success, result) {
|
||||
console.debug(`metrics/app/${login}/insights/plugins > ${plugin} > ${success ? "success" : "failure"}`)
|
||||
cache.put(`about.${login}.${plugin}`, result)
|
||||
cache.put(`insights.${login}.${plugin}`, result)
|
||||
},
|
||||
}
|
||||
;(async () => {
|
||||
try {
|
||||
const json = await metrics.insights({login}, {graphql, rest, conf, callbacks}, {Plugins, Templates})
|
||||
//Cache
|
||||
cache.put(`about.${login}`, json)
|
||||
cache.put(`insights.${login}`, json)
|
||||
if ((!debug) && (cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
|
||||
cache.put(`insights.${login}`, json, maxage > 0 ? maxage : cached)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -268,8 +271,22 @@ export default async function({sandbox = false} = {}) {
|
||||
_requests_refresh = true
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
app.get("/about/*", (req, res) => res.redirect(req.path.replace("/about/", "/insights/")))
|
||||
app.get("/insights/*", (req, res) => res.status(405).send("Method not allowed: this endpoint is not available"))
|
||||
}
|
||||
|
||||
//Metrics
|
||||
//Metrics embed
|
||||
if (conf.settings.modes.includes("embed")) {
|
||||
console.debug("metrics/app > setup embed mode")
|
||||
//Static routes
|
||||
app.get("/embed/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/embed/index.html`))
|
||||
app.get("/embed/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/embed/index.html`))
|
||||
app.use("/.placeholders", express.static(`${conf.paths.statics}/embed/placeholders`))
|
||||
app.get("/.js/embed/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/embed/app.js`))
|
||||
app.get("/.js/embed/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/embed/app.placeholder.js`))
|
||||
//App routes
|
||||
app.get("/:login/:repository?", ...middlewares, async (req, res) => {
|
||||
//Request params
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
@@ -319,10 +336,11 @@ export default async function({sandbox = false} = {}) {
|
||||
//Render
|
||||
const q = req.query
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth: Infinity, maxStringLength: 256})}`)
|
||||
if ((q["config.presets"]) && (conf.settings.extras?.presets ?? conf.settings.extras?.default ?? false)) {
|
||||
if ((q["config.presets"]) && ((conf.settings.extras?.features?.includes("metrics.setup.community.presets"))||(conf.settings.extras?.features === true)||(conf.settings.extras?.default))) {
|
||||
console.debug(`metrics/app/${login} > presets have been specified, loading them`)
|
||||
Object.assign(q, await presets(q["config.presets"]))
|
||||
}
|
||||
const convert = conf.settings.outputs.includes(q["config.output"]) ? q["config.output"] : conf.settings.outputs[0]
|
||||
const {rendered, mime} = await metrics({login, q}, {
|
||||
graphql,
|
||||
rest,
|
||||
@@ -330,7 +348,7 @@ export default async function({sandbox = false} = {}) {
|
||||
conf,
|
||||
die: q["plugins.errors.fatal"] ?? false,
|
||||
verify: q.verify ?? false,
|
||||
convert: ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
|
||||
convert: convert !== "auto" ? convert : null,
|
||||
}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug) && (cached)) {
|
||||
@@ -374,19 +392,39 @@ export default async function({sandbox = false} = {}) {
|
||||
_requests_refresh = true
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
app.get("/embed/*", (req, res) => res.status(405).send("Method not allowed: this endpoint is not available"))
|
||||
}
|
||||
|
||||
//Listen
|
||||
app.listen(port, () =>
|
||||
console.log([
|
||||
"───────────────────────────────────────────────────────────────────",
|
||||
"── Server configuration ───────────────────────────────────────────",
|
||||
`Listening on port │ ${port}`,
|
||||
`Debug mode │ ${debug}`,
|
||||
`Mocked data │ ${conf.settings.mocked ?? false}`,
|
||||
`Modes │ ${conf.settings.modes}`,
|
||||
"── Server capacity ───────────────────────────────────────────────",
|
||||
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`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.map(({name}) => name).join(", ")}`,
|
||||
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth: Infinity, maxStringLength: 256}) : "(enabled)"}`,
|
||||
`Max repositories per user │ ${conf.settings.repositories}`,
|
||||
"── Render settings ────────────────────────────────────────────────",
|
||||
`Cached time │ ${cached} seconds`,
|
||||
`SVG optimization │ ${conf.settings.optimize ?? false}`,
|
||||
`Allowed outputs │ ${conf.settings.outputs.join(", ")}`,
|
||||
`Padding │ ${conf.settings.padding}`,
|
||||
"── Sandbox ────────────────────────────────────────────────────────",
|
||||
`Debug │ ${debug}`,
|
||||
`Debug (puppeteer) │ ${conf.settings["debug.headless"] ?? false}`,
|
||||
`Mocked data │ ${conf.settings.mocked ?? false}`,
|
||||
"── Content ────────────────────────────────────────────────────────",
|
||||
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
|
||||
`Templates enabled │ ${templates.filter(({enabled}) => enabled).map(({name}) => name).join(", ")}`,
|
||||
"── Extras ─────────────────────────────────────────────────────────",
|
||||
`Default │ ${conf.settings.extras.default ?? false}`,
|
||||
`Features │ ${conf.settings.extras.features ?? "(none)"}`,
|
||||
"───────────────────────────────────────────────────────────────────",
|
||||
"Server ready !",
|
||||
].join("\n")))
|
||||
}
|
||||
|
||||
23
source/app/web/settings.example.json
generated
@@ -2,7 +2,8 @@
|
||||
"//": "Example of configuration for metrics web instance",
|
||||
"//": "====================================================================",
|
||||
|
||||
"token": "MY GITHUB API TOKEN", "//": "GitHub Personal Token (required)",
|
||||
"token": "GITHUB API TOKEN", "//": "GitHub Personal Token (required)",
|
||||
"modes": ["embed", "insights"], "//": "Web instance enabled modes",
|
||||
"restricted": [], "//": "Authorized users (empty to disable)",
|
||||
"maxusers": 0, "//": "Maximum users, (0 to disable)",
|
||||
"cached": 3600000, "//": "Cache time rendered metrics (0 to disable)",
|
||||
@@ -14,6 +15,7 @@
|
||||
"mocked": false, "//": "Use mocked data instead of live APIs (use 'force' to use mocked token even if real token are defined)",
|
||||
"repositories": 100, "//": "Number of repositories to use",
|
||||
"padding": ["0", "8 + 11%"], "//": "Image padding (default)",
|
||||
"outputs": ["svg", "png", "json"], "//": "Image output formats (empty to enable all)",
|
||||
"hosted": {
|
||||
"by": "", "//": "Web instance host (displayed in footer)",
|
||||
"link": "", "//": "Web instance host link (displayed in footer)"
|
||||
@@ -28,9 +30,22 @@
|
||||
"extras": {
|
||||
"default": false, "//": "Default extras state (advised to let 'false' unless in debug mode)",
|
||||
"presets": false, "//": "Allow use of 'config.presets' option",
|
||||
"css": false, "//": "Allow use of 'extras.css' option",
|
||||
"js": false, "//": "Allow use of 'extras.js' option",
|
||||
"features": false, "//": "Enable extra features (advised to let 'false' on web instances)"
|
||||
"features": false, "//": "Enable extra features (advised to let 'false' on web instances), see below for supported features",
|
||||
"//": "________________________________________________________________________",
|
||||
"//": "metrics.setup.community.templates | Allow community templates download",
|
||||
"//": "metrics.setup.community.presets | Allow community presets usage",
|
||||
"//": "metrics.api.github.overuse | Allow GitHub API intensive requests",
|
||||
"//": "metrics.cpu.overuse | Allow CPU intensive requests",
|
||||
"//": "metrics.run.tempdir | Allow access to temporary directory (I/O operations may be performed)",
|
||||
"//": "metrics.run.git | Allow to run git (needs to be installed)",
|
||||
"//": "metrics.run.licensed | Allow to run licensed (needs to be installed)",
|
||||
"//": "metrics.run.user.cmd | Allow to run ANY command by user (USE WITH CAUTION!)",
|
||||
"//": "metrics.run.puppeteer.scrapping | Allow to run puppeteer to scrape data",
|
||||
"//": "metrics.run.puppeteer.user.css | Allow to run CSS by user during puppeteer render",
|
||||
"//": "metrics.run.puppeteer.user.js | Allow to run JavaScript by user during puppeteer render",
|
||||
"//": "metrics.npm.optional.chartist | Allow use of chartist (needs to be installed)",
|
||||
"//": "metrics.npm.optional.gifencoder | Allow use of gifencoder (needs to be installed)",
|
||||
"//": "metrics.npm.optional.libxmljs2 | Allow use of libxmljs2 (needs to be installed)"
|
||||
},
|
||||
"plugins.default": false, "//": "Default plugin state (advised to let 'false' unless in debug mode)",
|
||||
"plugins": { "//": "Global plugin configuration",
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
;(async function() {
|
||||
//Init
|
||||
const {data: metadata} = await axios.get("/.plugins.metadata")
|
||||
delete metadata.core.web.output
|
||||
delete metadata.core.web.twemojis
|
||||
//App
|
||||
return new Vue({
|
||||
//Initialization
|
||||
@@ -62,7 +58,6 @@
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
components: {Prism: PrismComponent},
|
||||
//Watchers
|
||||
watch: {
|
||||
tab: {
|
||||
@@ -86,244 +81,20 @@
|
||||
data: {
|
||||
version: "",
|
||||
user: "",
|
||||
mode: "metrics",
|
||||
tab: "overview",
|
||||
palette: "light",
|
||||
clipboard: null,
|
||||
requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
|
||||
cached: new Map(),
|
||||
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
|
||||
metadata: Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
|
||||
|
||||
hosted: null,
|
||||
docs: {
|
||||
overview: {
|
||||
link: "https://github.com/lowlighter/metrics#-documentation",
|
||||
name: "Complete documentation",
|
||||
},
|
||||
markdown: {
|
||||
link: "https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/documentation/setup/shared.md",
|
||||
name: "Setup using the shared instance",
|
||||
},
|
||||
action: {
|
||||
link: "https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/documentation/setup/action.md",
|
||||
name: "Setup using GitHub Action on a profile repository",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
base: {},
|
||||
list: [],
|
||||
categories: [],
|
||||
enabled: {},
|
||||
descriptions: {
|
||||
base: "🗃️ Base content",
|
||||
"base.header": "Header",
|
||||
"base.activity": "Account activity",
|
||||
"base.community": "Community stats",
|
||||
"base.repositories": "Repositories metrics",
|
||||
"base.metadata": "Metadata",
|
||||
...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name])),
|
||||
},
|
||||
options: {
|
||||
descriptions: {...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
|
||||
...(Object.fromEntries(
|
||||
Object.entries(
|
||||
Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)),
|
||||
)
|
||||
.map(([key, {defaulted}]) => [key, defaulted]),
|
||||
)),
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
list: [],
|
||||
selected: "classic",
|
||||
placeholder: {
|
||||
timeout: null,
|
||||
image: "",
|
||||
},
|
||||
descriptions: {
|
||||
classic: "Classic template",
|
||||
terminal: "Terminal template",
|
||||
markdown: "(hidden)",
|
||||
repository: "(hidden)",
|
||||
},
|
||||
},
|
||||
generated: {
|
||||
pending: false,
|
||||
content: "",
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
//Computed data
|
||||
computed: {
|
||||
//Unusable plugins
|
||||
unusable() {
|
||||
return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
|
||||
},
|
||||
//User's avatar
|
||||
avatar() {
|
||||
return this.generated.content ? `https://github.com/${this.user}.png` : null
|
||||
},
|
||||
//User's repository
|
||||
repo() {
|
||||
return `https://github.com/${this.user}/${this.user}`
|
||||
},
|
||||
//Endpoint to use for computed metrics
|
||||
url() {
|
||||
//Plugins enabled
|
||||
const plugins = Object.entries(this.plugins.enabled)
|
||||
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
|
||||
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
|
||||
.map(([key, value]) => `${key}=${+value}`)
|
||||
//Plugins options
|
||||
const options = Object.entries(this.plugins.options)
|
||||
.filter(([key, value]) => `${value}`.length)
|
||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Base options
|
||||
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Config
|
||||
const config = Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||
//Template
|
||||
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
|
||||
//Generated url
|
||||
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
|
||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||
},
|
||||
//Embedded generated code
|
||||
embed() {
|
||||
return ``
|
||||
},
|
||||
//Token scopes
|
||||
scopes() {
|
||||
return new Set([
|
||||
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).flatMap(([key]) => metadata[key].scopes),
|
||||
...(Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).length ? metadata.base.scopes : []),
|
||||
])
|
||||
},
|
||||
//GitHub action auto-generated code
|
||||
action() {
|
||||
return [
|
||||
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
|
||||
`name: Metrics`,
|
||||
`on:`,
|
||||
` # Schedule updates (each hour)`,
|
||||
` schedule: [{cron: "0 * * * *"}]`,
|
||||
` # Lines below let you run workflow manually and on each commit`,
|
||||
` workflow_dispatch:`,
|
||||
` push: {branches: ["master", "main"]}`,
|
||||
`jobs:`,
|
||||
` github-metrics:`,
|
||||
` runs-on: ubuntu-latest`,
|
||||
` permissions:`,
|
||||
` contents: write`,
|
||||
` steps:`,
|
||||
` - uses: lowlighter/metrics@latest`,
|
||||
` with:`,
|
||||
...(this.scopes.size
|
||||
? [
|
||||
` # Your GitHub token`,
|
||||
` # The following scopes are required:`,
|
||||
...[...this.scopes].map(scope => ` # - ${scope}${scope === "public_access" ? " (default scope)" : ""}`),
|
||||
` # The following additional scopes may be required:`,
|
||||
` # - read:org (for organization related metrics)`,
|
||||
` # - read:user (for user related data)`,
|
||||
` # - read:packages (for some packages related data)`,
|
||||
` # - repo (optional, if you want to include private repositories)`,
|
||||
]
|
||||
: [
|
||||
` # Current configuration doesn't require a GitHub token`,
|
||||
]),
|
||||
` token: ${this.scopes.size ? `${"$"}{{ secrets.METRICS_TOKEN }}` : "NOT_NEEDED"}`,
|
||||
``,
|
||||
` # Options`,
|
||||
` user: ${this.user}`,
|
||||
` template: ${this.templates.selected}`,
|
||||
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ") || '""'}`,
|
||||
...[
|
||||
...Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => ` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).map(([key]) => ` plugin_${key}: yes`),
|
||||
...Object.entries(this.plugins.options).filter(([key, value]) => (value) && (!(key in metadata.base.web))).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
].sort(),
|
||||
].join("\n")
|
||||
},
|
||||
//Configurable plugins
|
||||
configure() {
|
||||
//Check enabled plugins
|
||||
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value) && (key !== "base")).map(([key, value]) => key)
|
||||
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
|
||||
//Search related options
|
||||
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => (filter.test(key)) && (!(key in metadata.base.web)))
|
||||
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
//Return object
|
||||
const configure = Object.fromEntries(entries)
|
||||
return Object.keys(configure).length ? configure : null
|
||||
},
|
||||
//Is in preview mode
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
},
|
||||
//Rate limit reset
|
||||
rlreset() {
|
||||
const reset = new Date(Math.max(this.requests.graphql.reset, this.requests.rest.reset))
|
||||
return `${reset.getHours()}:${reset.getMinutes()}`
|
||||
},
|
||||
},
|
||||
//Methods
|
||||
methods: {
|
||||
//Refresh computed properties
|
||||
async refresh() {
|
||||
const keys = {action: ["scopes", "action"], markdown: ["url", "embed"]}[this.tab]
|
||||
if (keys) {
|
||||
for (const key of keys)
|
||||
this._computedWatchers[key]?.run()
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
//Load and render placeholder image
|
||||
async mock({timeout = 600} = {}) {
|
||||
this.refresh()
|
||||
clearTimeout(this.templates.placeholder.timeout)
|
||||
this.templates.placeholder.timeout = setTimeout(async () => {
|
||||
this.templates.placeholder.image = await placeholder(this)
|
||||
this.generated.content = ""
|
||||
this.generated.error = null
|
||||
}, timeout)
|
||||
},
|
||||
//Resize mock image
|
||||
mockresize() {
|
||||
const svg = document.querySelector(".preview .image svg")
|
||||
if ((svg) && (svg.getAttribute("height") == 99999)) {
|
||||
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y - svg.getBoundingClientRect()?.y
|
||||
if (Number.isFinite(height))
|
||||
svg.setAttribute("height", height)
|
||||
}
|
||||
},
|
||||
//Generate metrics and flush cache
|
||||
async generate() {
|
||||
//Avoid requests spamming
|
||||
if (this.generated.pending)
|
||||
return
|
||||
this.generated.pending = true
|
||||
//Compute metrics
|
||||
try {
|
||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
||||
this.generated.content = (await axios.get(this.url)).data
|
||||
this.generated.error = null
|
||||
}
|
||||
catch (error) {
|
||||
this.generated.error = {code: error.response.status, message: error.response.data}
|
||||
}
|
||||
finally {
|
||||
this.generated.pending = false
|
||||
try {
|
||||
const {data: requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
})()
|
||||
|
||||
328
source/app/web/statics/embed/app.js
Normal file
@@ -0,0 +1,328 @@
|
||||
;(async function() {
|
||||
//Init
|
||||
const {data: metadata} = await axios.get("/.plugins.metadata")
|
||||
delete metadata.core.web.output
|
||||
delete metadata.core.web.twemojis
|
||||
//App
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el: "main",
|
||||
async mounted() {
|
||||
//Interpolate config from browser
|
||||
try {
|
||||
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
this.palette = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
}
|
||||
catch (error) {}
|
||||
//Init
|
||||
await Promise.all([
|
||||
//GitHub limit tracker
|
||||
(async () => {
|
||||
const {data: requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
})(),
|
||||
//Templates
|
||||
(async () => {
|
||||
const {data: templates} = await axios.get("/.templates")
|
||||
templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name))
|
||||
this.templates.list = templates
|
||||
this.templates.selected = templates[0]?.name || "classic"
|
||||
})(),
|
||||
//Plugins
|
||||
(async () => {
|
||||
const {data: plugins} = await axios.get("/.plugins")
|
||||
this.plugins.list = plugins.filter(({name}) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
|
||||
const categories = [...new Set(this.plugins.list.map(({category}) => category))]
|
||||
this.plugins.categories = Object.fromEntries(categories.map(category => [category, this.plugins.list.filter(value => category === value.category)]))
|
||||
})(),
|
||||
//Base
|
||||
(async () => {
|
||||
const {data: base} = await axios.get("/.plugins.base")
|
||||
this.plugins.base = base
|
||||
this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true]))
|
||||
})(),
|
||||
//Version
|
||||
(async () => {
|
||||
const {data: version} = await axios.get("/.version")
|
||||
this.version = `v${version}`
|
||||
})(),
|
||||
//Hosted
|
||||
(async () => {
|
||||
const {data: hosted} = await axios.get("/.hosted")
|
||||
this.hosted = hosted
|
||||
})(),
|
||||
])
|
||||
//Generate placeholder
|
||||
this.mock({timeout: 200})
|
||||
setInterval(() => {
|
||||
const marker = document.querySelector("#metrics-end")
|
||||
if (marker) {
|
||||
this.mockresize()
|
||||
marker.remove()
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
components: {Prism: PrismComponent},
|
||||
//Watchers
|
||||
watch: {
|
||||
tab: {
|
||||
immediate: true,
|
||||
handler(current) {
|
||||
if (current === "action")
|
||||
this.clipboard = new ClipboardJS(".copy-action")
|
||||
else
|
||||
this.clipboard?.destroy()
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
immediate: true,
|
||||
handler(current, previous) {
|
||||
document.querySelector("body").classList.remove(previous)
|
||||
document.querySelector("body").classList.add(current)
|
||||
},
|
||||
},
|
||||
},
|
||||
//Data initialization
|
||||
data: {
|
||||
version: "",
|
||||
user: "",
|
||||
tab: "overview",
|
||||
palette: "light",
|
||||
clipboard: null,
|
||||
requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
|
||||
cached: new Map(),
|
||||
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
|
||||
metadata: Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
|
||||
hosted: null,
|
||||
docs: {
|
||||
overview: {
|
||||
link: "https://github.com/lowlighter/metrics#-documentation",
|
||||
name: "Complete documentation",
|
||||
},
|
||||
markdown: {
|
||||
link: "https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/documentation/setup/shared.md",
|
||||
name: "Setup using the shared instance",
|
||||
},
|
||||
action: {
|
||||
link: "https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/documentation/setup/action.md",
|
||||
name: "Setup using GitHub Action on a profile repository",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
base: {},
|
||||
list: [],
|
||||
categories: [],
|
||||
enabled: {},
|
||||
descriptions: {
|
||||
base: "🗃️ Base content",
|
||||
"base.header": "Header",
|
||||
"base.activity": "Account activity",
|
||||
"base.community": "Community stats",
|
||||
"base.repositories": "Repositories metrics",
|
||||
"base.metadata": "Metadata",
|
||||
...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name])),
|
||||
},
|
||||
options: {
|
||||
descriptions: {...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
|
||||
...(Object.fromEntries(
|
||||
Object.entries(
|
||||
Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)),
|
||||
)
|
||||
.map(([key, {defaulted}]) => [key, defaulted]),
|
||||
)),
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
list: [],
|
||||
selected: "classic",
|
||||
placeholder: {
|
||||
timeout: null,
|
||||
image: "",
|
||||
},
|
||||
descriptions: {
|
||||
classic: "Classic template",
|
||||
terminal: "Terminal template",
|
||||
markdown: "(hidden)",
|
||||
repository: "(hidden)",
|
||||
},
|
||||
},
|
||||
generated: {
|
||||
pending: false,
|
||||
content: "",
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
//Computed data
|
||||
computed: {
|
||||
//Unusable plugins
|
||||
unusable() {
|
||||
return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
|
||||
},
|
||||
//User's avatar
|
||||
avatar() {
|
||||
return this.generated.content ? `https://github.com/${this.user}.png` : null
|
||||
},
|
||||
//User's repository
|
||||
repo() {
|
||||
return `https://github.com/${this.user}/${this.user}`
|
||||
},
|
||||
//Endpoint to use for computed metrics
|
||||
url() {
|
||||
//Plugins enabled
|
||||
const plugins = Object.entries(this.plugins.enabled)
|
||||
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
|
||||
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
|
||||
.map(([key, value]) => `${key}=${+value}`)
|
||||
//Plugins options
|
||||
const options = Object.entries(this.plugins.options)
|
||||
.filter(([key, value]) => `${value}`.length)
|
||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Base options
|
||||
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Config
|
||||
const config = Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||
//Template
|
||||
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
|
||||
//Generated url
|
||||
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
|
||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||
},
|
||||
//Embedded generated code
|
||||
embed() {
|
||||
return ``
|
||||
},
|
||||
//Token scopes
|
||||
scopes() {
|
||||
return new Set([
|
||||
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).flatMap(([key]) => metadata[key].scopes),
|
||||
...(Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).length ? metadata.base.scopes : []),
|
||||
])
|
||||
},
|
||||
//GitHub action auto-generated code
|
||||
action() {
|
||||
return [
|
||||
`# Visit https://github.com/lowlighter/metrics#-documentation for full reference`,
|
||||
`name: Metrics`,
|
||||
`on:`,
|
||||
` # Schedule updates (each hour)`,
|
||||
` schedule: [{cron: "0 * * * *"}]`,
|
||||
` # Lines below let you run workflow manually and on each commit`,
|
||||
` workflow_dispatch:`,
|
||||
` push: {branches: ["master", "main"]}`,
|
||||
`jobs:`,
|
||||
` github-metrics:`,
|
||||
` runs-on: ubuntu-latest`,
|
||||
` permissions:`,
|
||||
` contents: write`,
|
||||
` steps:`,
|
||||
` - uses: lowlighter/metrics@latest`,
|
||||
` with:`,
|
||||
...(this.scopes.size
|
||||
? [
|
||||
` # Your GitHub token`,
|
||||
` # The following scopes are required:`,
|
||||
...[...this.scopes].map(scope => ` # - ${scope}${scope === "public_access" ? " (default scope)" : ""}`),
|
||||
` # The following additional scopes may be required:`,
|
||||
` # - read:org (for organization related metrics)`,
|
||||
` # - read:user (for user related data)`,
|
||||
` # - read:packages (for some packages related data)`,
|
||||
` # - repo (optional, if you want to include private repositories)`,
|
||||
]
|
||||
: [
|
||||
` # Current configuration doesn't require a GitHub token`,
|
||||
]),
|
||||
` token: ${this.scopes.size ? `${"$"}{{ secrets.METRICS_TOKEN }}` : "NOT_NEEDED"}`,
|
||||
``,
|
||||
` # Options`,
|
||||
...(this.user ? [` user: ${this.user}`] : []),
|
||||
` template: ${this.templates.selected}`,
|
||||
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ") || '""'}`,
|
||||
...[
|
||||
...Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => ` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).map(([key]) => ` plugin_${key}: yes`),
|
||||
...Object.entries(this.plugins.options).filter(([key, value]) => (value) && (!(key in metadata.base.web))).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
|
||||
].sort(),
|
||||
].join("\n")
|
||||
},
|
||||
//Configurable plugins
|
||||
configure() {
|
||||
//Check enabled plugins
|
||||
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value) && (key !== "base")).map(([key, value]) => key)
|
||||
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
|
||||
//Search related options
|
||||
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => (filter.test(key)) && (!(key in metadata.base.web)))
|
||||
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
//Return object
|
||||
const configure = Object.fromEntries(entries)
|
||||
return Object.keys(configure).length ? configure : null
|
||||
},
|
||||
//Is in preview mode
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
},
|
||||
//Rate limit reset
|
||||
rlreset() {
|
||||
const reset = new Date(Math.max(this.requests.graphql.reset, this.requests.rest.reset))
|
||||
return `${reset.getHours()}:${reset.getMinutes()}`
|
||||
},
|
||||
},
|
||||
//Methods
|
||||
methods: {
|
||||
//Refresh computed properties
|
||||
async refresh() {
|
||||
const keys = {action: ["scopes", "action"], markdown: ["url", "embed"]}[this.tab]
|
||||
if (keys) {
|
||||
for (const key of keys)
|
||||
this._computedWatchers[key]?.run()
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
//Load and render placeholder image
|
||||
async mock({timeout = 600} = {}) {
|
||||
this.refresh()
|
||||
clearTimeout(this.templates.placeholder.timeout)
|
||||
this.templates.placeholder.timeout = setTimeout(async () => {
|
||||
this.templates.placeholder.image = await placeholder(this)
|
||||
this.generated.content = ""
|
||||
this.generated.error = null
|
||||
}, timeout)
|
||||
},
|
||||
//Resize mock image
|
||||
mockresize() {
|
||||
const svg = document.querySelector(".preview .image svg")
|
||||
if ((svg) && (svg.getAttribute("height") == 99999)) {
|
||||
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y - svg.getBoundingClientRect()?.y
|
||||
if (Number.isFinite(height))
|
||||
svg.setAttribute("height", height)
|
||||
}
|
||||
},
|
||||
//Generate metrics and flush cache
|
||||
async generate() {
|
||||
//Avoid requests spamming
|
||||
if (this.generated.pending)
|
||||
return
|
||||
this.generated.pending = true
|
||||
//Compute metrics
|
||||
try {
|
||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
||||
this.generated.content = (await axios.get(this.url)).data
|
||||
this.generated.error = null
|
||||
}
|
||||
catch (error) {
|
||||
this.generated.error = {code: error.response.status, message: error.response.data}
|
||||
}
|
||||
finally {
|
||||
this.generated.pending = false
|
||||
try {
|
||||
const {data: requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
})()
|
||||
201
source/app/web/statics/embed/index.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Metrics</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !">
|
||||
<meta name="author" content="lowlighter">
|
||||
<meta property="og:image" content="/.opengraph.png">
|
||||
<link rel="icon" href="/.favicon.png">
|
||||
<link rel="stylesheet" href="/.css/style.vars.css">
|
||||
<link rel="stylesheet" href="/.css/style.css">
|
||||
<link rel="stylesheet" href="/.css/style.prism.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue app -->
|
||||
<main :class="[palette]">
|
||||
<template>
|
||||
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
||||
<a href="https://github.com/lowlighter/metrics">Metrics {{ version }}</a>
|
||||
</header>
|
||||
|
||||
<div class="ui top">
|
||||
<aside></aside>
|
||||
<nav>
|
||||
<div @click="tab = 'overview'" :class="{active:tab === 'overview'}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
Metrics preview
|
||||
</div>
|
||||
<div @click="tab = 'action'" :class="{active:tab === 'action'}">
|
||||
<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 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
|
||||
Action code
|
||||
</div>
|
||||
<div @click="tab = 'markdown'" :class="{active:tab === 'markdown'}">
|
||||
<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>
|
||||
Markdown code
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="ui">
|
||||
|
||||
<aside>
|
||||
|
||||
<div class="ui-avatar" :style="{backgroundImage:avatar ? `url(${avatar})` : 'none'}"></div>
|
||||
|
||||
<input type="text" v-model="user" placeholder="Your GitHub username" :disabled="generated.pending" @keyup.enter="(!user)||(generated.pending)||(unusable.length > 0)||(!requests.rest.remaining)||(!requests.graphql.remaining) ? null : generate()">
|
||||
<button @click="generate" :disabled="(!user)||(generated.pending)||(unusable.length > 0)||(!requests.rest.remaining)||(!requests.graphql.remaining)">
|
||||
<template v-if="generated.pending">
|
||||
Generating metrics<span class="loading"></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate your metrics!
|
||||
</template>
|
||||
</button>
|
||||
<small :class="{'error-text':(!requests.rest.remaining)||(!requests.graphql.remaining)}">Remaining GitHub requests:</small>
|
||||
<small>{{ requests.rest.remaining }} REST / {{ requests.graphql.remaining }} GraphQL</small>
|
||||
<small class="warning" v-if="preview">
|
||||
Metrics are rendered by <a href="https://metrics.lecoq.io/">metrics.lecoq.io</a> in preview mode.
|
||||
Any backend editions won't be reflected but client-side rendering can still be tested.
|
||||
</small>
|
||||
<div class="warning" v-if="unusable.length">
|
||||
The following plugins are not available on this web instance: {{ unusable.join(", ") }}
|
||||
</div>
|
||||
<div class="warning" v-if="(!requests.rest.remaining)||(!requests.graphql.remaining)">
|
||||
This web instance has run out of GitHub API requests.
|
||||
Please wait until {{ rlreset }} to generate metrics again.
|
||||
</div>
|
||||
|
||||
<div class="configuration">
|
||||
<b>🖼️ Template</b>
|
||||
<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 this web instance, use GitHub actions instead!' : ''">
|
||||
<input type="radio" v-model="templates.selected" :value="template.name" @change="mock" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template.name] || template.name }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="configuration" v-if="plugins.base.length">
|
||||
<b>🗃️ Base content</b>
|
||||
<label v-for="part in plugins.base" :key="part">
|
||||
<input type="checkbox" v-model="plugins.enabled.base[part]" @change="mock" :disabled="generated.pending">
|
||||
<span>{{ plugins.descriptions[`base.${part}`] || `base.${part}` }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="configuration plugins" v-if="plugins.list.length">
|
||||
<b>🧩 Additional plugins</b>
|
||||
<template v-for="(category, name) in plugins.categories" :key="category">
|
||||
<details open>
|
||||
<summary>{{ name }}</summary>
|
||||
<label v-for="plugin in category" :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="mock" :disabled="generated.pending">
|
||||
<div>{{ plugins.descriptions[plugin.name] || plugin.name }}</div>
|
||||
</label>
|
||||
</details>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="configuration" v-if="configure">
|
||||
<b>🔧 Configure plugins</b>
|
||||
<template v-for="(input, key) in configure">
|
||||
<b v-if="typeof input === 'string'">{{ input }}</b>
|
||||
<label v-else class="option">
|
||||
<i>{{ input.text.split("\n")[0] }}</i>
|
||||
<input type="checkbox" v-if="input.type === 'boolean'" v-model="plugins.options[key]" @change="mock">
|
||||
<input type="number" v-else-if="input.type === 'number'" v-model="plugins.options[key]" @change="mock" :min="input.min" :max="input.max">
|
||||
<select v-else-if="input.type === 'select'" v-model="plugins.options[key]" @change="mock">
|
||||
<option v-for="value in input.values" :value="value">{{ value }}</option>
|
||||
</select>
|
||||
<input type="text" v-else v-model="plugins.options[key]" @change="mock" :placeholder="input.placeholder">
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="configuration">
|
||||
<details>
|
||||
<summary><b>⚙️ Additional settings</b></summary>
|
||||
<template v-for="{key, target} in [{key:'base', target:plugins.options}, {key:'core', target:config}]">
|
||||
<template v-for="(input, key) in metadata[key]">
|
||||
<label class="option">
|
||||
<i>{{ input.text.split("\n")[0] }}</i>
|
||||
<input type="checkbox" v-if="input.type === 'boolean'" v-model="target[key]" @change="mock">
|
||||
<input type="number" v-else-if="input.type === 'number'" v-model="target[key]" @change="mock" :min="input.min" :max="input.max">
|
||||
<select v-else-if="input.type === 'select'" v-model="target[key]" @change="mock">
|
||||
<option v-for="value in input.values" :value="value">{{ value }}</option>
|
||||
</select>
|
||||
<input type="text" v-else v-model="target[key]" @change="mock" :placeholder="input.placeholder">
|
||||
</label>
|
||||
</template>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<div class="preview">
|
||||
|
||||
<div class="readmes">
|
||||
<div class="readme">
|
||||
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg>
|
||||
<span>{{ user }}</span><span class="slash">/</span>README<span class="md">.md</span>
|
||||
</div>
|
||||
<div class="readme" v-if="tab in docs">
|
||||
<a :href="docs[tab].link">{{ docs[tab].name }}</a>
|
||||
</div>
|
||||
<div class="readme">
|
||||
<a href="https://github.com/lowlighter/metrics/discussions" target="_blank">Send feedback</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'overview'">
|
||||
<div class="alert error" v-if="generated.error">
|
||||
An error occurred while generating your metrics :(<br>
|
||||
<small>{{ generated.error.message }}</small>
|
||||
</div>
|
||||
<div class="image" :class="{pending:generated.pending}" v-html="generated.content||templates.placeholder.image"></div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'markdown'">
|
||||
Add the markdown below to your <i>README.md</i> <template v-if="user">at <a :href="repo">{{ user }}/{{ user }}</a></template>
|
||||
<div class="code">
|
||||
<Prism language="markdown" :code="embed"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'action'">
|
||||
<div>
|
||||
<button class="copy-action" data-clipboard-target=".code">Copy Action Code</button>
|
||||
</div>
|
||||
Create a new workflow with the following content <template v-if="user">at <a :href="repo">{{ user }}/{{ user }}</a></template>
|
||||
<div class="code">
|
||||
<Prism language="yaml" :code="action"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/lowlighter/metrics">Repository</a>
|
||||
<a href="https://github.com/lowlighter/metrics/blob/master/LICENSE">License</a>
|
||||
<a href="https://github.com/marketplace/actions/metrics-embed">GitHub Action</a>
|
||||
<span v-if="hosted">Hosted with ❤️ by <a :href="hosted.link">{{ hosted.by }}</a></span>
|
||||
</footer>
|
||||
|
||||
</template>
|
||||
</main>
|
||||
<!-- Scripts -->
|
||||
<script src="/.js/axios.min.js"></script>
|
||||
<script src="/.js/prism.min.js"></script>
|
||||
<script src="/.js/prism.markdown.min.js"></script>
|
||||
<script src="/.js/prism.yaml.min.js"></script>
|
||||
<script src="/.js/ejs.min.js"></script>
|
||||
<script src="/.js/faker.min.js?v=7.x" type="module"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/vue.prism.min.js"></script>
|
||||
<script src="/.js/clipboard.min.js"></script>
|
||||
<script src="/.js/embed/app.placeholder.js?v=3.26"></script>
|
||||
<script src="/.js/embed/app.js?v=3.26"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
@@ -10,7 +10,6 @@
|
||||
<link rel="icon" href="/.favicon.png">
|
||||
<link rel="stylesheet" href="/.css/style.vars.css">
|
||||
<link rel="stylesheet" href="/.css/style.css">
|
||||
<link rel="stylesheet" href="/.css/style.prism.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue app -->
|
||||
@@ -22,165 +21,11 @@
|
||||
<a href="https://github.com/lowlighter/metrics">Metrics {{ version }}</a>
|
||||
</header>
|
||||
|
||||
<div class="ui top">
|
||||
<aside></aside>
|
||||
<nav>
|
||||
<div @click="mode = 'metrics', tab = 'overview'" :class="{active:tab === 'overview'}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
Metrics preview
|
||||
</div>
|
||||
<div @click="(user)&&(mode === 'metrics') ? tab = 'action' : null" :class="{active:tab === 'action', disabled:(!user)||(mode !== 'metrics')}">
|
||||
<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 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
|
||||
Action code
|
||||
</div>
|
||||
<div @click="(user)&&(mode === 'metrics') ? tab = 'markdown' : null" :class="{active:tab === 'markdown', disabled:(!user)||(mode !== 'metrics')}">
|
||||
<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>
|
||||
Markdown code
|
||||
</div>
|
||||
<div @click="mode = 'insights', tab = 'insights'" :class="{active:tab === 'insights'}">
|
||||
<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>
|
||||
Metrics Insights
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="ui" v-if="mode === 'metrics'">
|
||||
|
||||
<aside>
|
||||
|
||||
<div class="ui-avatar" :style="{backgroundImage:avatar ? `url(${avatar})` : 'none'}"></div>
|
||||
|
||||
<input type="text" v-model="user" placeholder="Your GitHub username" :disabled="generated.pending" @keyup.enter="(!user)||(generated.pending)||(unusable.length > 0)||(!requests.rest.remaining)||(!requests.graphql.remaining) ? null : generate()">
|
||||
<button @click="generate" :disabled="(!user)||(generated.pending)||(unusable.length > 0)||(!requests.rest.remaining)||(!requests.graphql.remaining)">
|
||||
<template v-if="generated.pending">
|
||||
Generating metrics<span class="loading"></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
Generate your metrics!
|
||||
</template>
|
||||
</button>
|
||||
<small :class="{'error-text':(!requests.rest.remaining)||(!requests.graphql.remaining)}">Remaining GitHub requests:</small>
|
||||
<small>{{ requests.rest.remaining }} REST / {{ requests.graphql.remaining }} GraphQL</small>
|
||||
<small class="warning" v-if="preview">
|
||||
Metrics are rendered by <a href="https://metrics.lecoq.io/">metrics.lecoq.io</a> in preview mode.
|
||||
Any backend editions won't be reflected but client-side rendering can still be tested.
|
||||
</small>
|
||||
<div class="warning" v-if="unusable.length">
|
||||
The following plugins are not available on this web instance: {{ unusable.join(", ") }}
|
||||
</div>
|
||||
<div class="warning" v-if="(!requests.rest.remaining)||(!requests.graphql.remaining)">
|
||||
This web instance has run out of GitHub API requests.
|
||||
Please wait until {{ rlreset }} to generate metrics again.
|
||||
</div>
|
||||
|
||||
<div class="configuration">
|
||||
<b>🖼️ Template</b>
|
||||
<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 this web instance, use GitHub actions instead!' : ''">
|
||||
<input type="radio" v-model="templates.selected" :value="template.name" @change="mock" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template.name] || template.name }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="configuration" v-if="plugins.base.length">
|
||||
<b>🗃️ Base content</b>
|
||||
<label v-for="part in plugins.base" :key="part">
|
||||
<input type="checkbox" v-model="plugins.enabled.base[part]" @change="mock" :disabled="generated.pending">
|
||||
<span>{{ plugins.descriptions[`base.${part}`] || `base.${part}` }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="configuration plugins" v-if="plugins.list.length">
|
||||
<b>🧩 Additional plugins</b>
|
||||
<template v-for="(category, name) in plugins.categories" :key="category">
|
||||
<details open>
|
||||
<summary>{{ name }}</summary>
|
||||
<label v-for="plugin in category" :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="mock" :disabled="generated.pending">
|
||||
<div>{{ plugins.descriptions[plugin.name] || plugin.name }}</div>
|
||||
</label>
|
||||
</details>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="configuration" v-if="configure">
|
||||
<b>🔧 Configure plugins</b>
|
||||
<template v-for="(input, key) in configure">
|
||||
<b v-if="typeof input === 'string'">{{ input }}</b>
|
||||
<label v-else class="option">
|
||||
<i>{{ input.text.split("\n")[0] }}</i>
|
||||
<input type="checkbox" v-if="input.type === 'boolean'" v-model="plugins.options[key]" @change="mock">
|
||||
<input type="number" v-else-if="input.type === 'number'" v-model="plugins.options[key]" @change="mock" :min="input.min" :max="input.max">
|
||||
<select v-else-if="input.type === 'select'" v-model="plugins.options[key]" @change="mock">
|
||||
<option v-for="value in input.values" :value="value">{{ value }}</option>
|
||||
</select>
|
||||
<input type="text" v-else v-model="plugins.options[key]" @change="mock" :placeholder="input.placeholder">
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="configuration">
|
||||
<details>
|
||||
<summary><b>⚙️ Additional settings</b></summary>
|
||||
<template v-for="{key, target} in [{key:'base', target:plugins.options}, {key:'core', target:config}]">
|
||||
<template v-for="(input, key) in metadata[key]">
|
||||
<label class="option">
|
||||
<i>{{ input.text.split("\n")[0] }}</i>
|
||||
<input type="checkbox" v-if="input.type === 'boolean'" v-model="target[key]" @change="mock">
|
||||
<input type="number" v-else-if="input.type === 'number'" v-model="target[key]" @change="mock" :min="input.min" :max="input.max">
|
||||
<select v-else-if="input.type === 'select'" v-model="target[key]" @change="mock">
|
||||
<option v-for="value in input.values" :value="value">{{ value }}</option>
|
||||
</select>
|
||||
<input type="text" v-else v-model="target[key]" @change="mock" :placeholder="input.placeholder">
|
||||
</label>
|
||||
</template>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<div class="preview">
|
||||
|
||||
<div class="readmes">
|
||||
<div class="readme">
|
||||
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg>
|
||||
<span>{{ user }}</span><span class="slash">/</span>README<span class="md">.md</span>
|
||||
</div>
|
||||
<div class="readme" v-if="tab in docs">
|
||||
<a :href="docs[tab].link">{{ docs[tab].name }}</a>
|
||||
</div>
|
||||
<div class="readme">
|
||||
<a href="https://github.com/lowlighter/metrics/discussions" target="_blank">Send feedback</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'overview'">
|
||||
<div class="error" v-if="generated.error">
|
||||
An error occurred while generating your metrics :(<br>
|
||||
<small>{{ generated.error.message }}</small>
|
||||
</div>
|
||||
<div class="image" :class="{pending:generated.pending}" v-html="generated.content||templates.placeholder.image"></div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'markdown'">
|
||||
Add the markdown below to your <i>README.md</i> at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="markdown" :code="embed"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'action'">
|
||||
<div>
|
||||
<button class="copy-action" data-clipboard-target=".code">Copy Action Code</button>
|
||||
</div>
|
||||
Create a new workflow with the following content at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="yaml" :code="action"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<iframe v-else src="/about?embed=1" frameborder="0"></iframe>
|
||||
<main>
|
||||
<section class="container center">
|
||||
Hi
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/lowlighter/metrics">Repository</a>
|
||||
@@ -193,15 +38,7 @@
|
||||
</main>
|
||||
<!-- Scripts -->
|
||||
<script src="/.js/axios.min.js"></script>
|
||||
<script src="/.js/prism.min.js"></script>
|
||||
<script src="/.js/prism.markdown.min.js"></script>
|
||||
<script src="/.js/prism.yaml.min.js"></script>
|
||||
<script src="/.js/ejs.min.js"></script>
|
||||
<script src="/.js/faker.min.js?v=7.x" type="module"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/vue.prism.min.js"></script>
|
||||
<script src="/.js/clipboard.min.js"></script>
|
||||
<script src="/.js/app.placeholder.js?v=3.26"></script>
|
||||
<script src="/.js/app.js?v=3.26"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="icon" href="/.favicon.png">
|
||||
<link rel="stylesheet" href="/.css/style.vars.css">
|
||||
<link rel="stylesheet" href="/.css/style.css">
|
||||
<link rel="stylesheet" href="/about/.statics/style.css">
|
||||
<link rel="stylesheet" href="/insights/.statics/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue app -->
|
||||
@@ -585,6 +585,6 @@
|
||||
<!-- Scripts -->
|
||||
<script src="/.js/axios.min.js"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/about/.statics/script.js?v=3.26"></script>
|
||||
<script src="/insights/.statics/script.js?v=3.26"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,7 +15,7 @@
|
||||
this.localstorage = !!(new URLSearchParams(location.search).get("localstorage"))
|
||||
//User
|
||||
const user = location.pathname.split("/").pop()
|
||||
if ((user) && (user !== "about")) {
|
||||
if ((user) && (!["about", "insights"].includes(user))) {
|
||||
this.user = user
|
||||
await this.search()
|
||||
}
|
||||
@@ -99,7 +99,7 @@
|
||||
this.loaded = ["base", ...Object.keys(this.metrics?.rendered?.plugins ?? {})]
|
||||
return
|
||||
}
|
||||
const {processing, ...data} = (await axios.get(`/about/query/${this.user}`)).data
|
||||
const {processing, ...data} = (await axios.get(`/insights/query/${this.user}`)).data
|
||||
if (processing) {
|
||||
let completed = 0
|
||||
this.progress = 1 / (data.plugins.length + 1)
|
||||
@@ -109,7 +109,7 @@
|
||||
return
|
||||
do {
|
||||
try {
|
||||
const {data} = await axios.get(`/about/query/${this.user}/${plugin}`)
|
||||
const {data} = await axios.get(`/insights/query/${this.user}/${plugin}`)
|
||||
if (!data)
|
||||
throw new Error(`${plugin}: no data`)
|
||||
if (plugin === "base")
|
||||
@@ -220,7 +220,7 @@
|
||||
return {login, name, avatar: this.metrics?.rendered.computed.avatar, type: this.metrics?.rendered.account}
|
||||
},
|
||||
url() {
|
||||
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
|
||||
return `${window.location.protocol}//${window.location.host}/insights/${this.user}`
|
||||
},
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
@@ -281,7 +281,7 @@
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error {
|
||||
.alert.error {
|
||||
padding: 1.25rem 1rem;
|
||||
background-image: linear-gradient(var(--color-alert-error-bg),var(--color-alert-error-bg));
|
||||
color: var(--color-alert-error-text);
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import * as compute from "./list/index.mjs"
|
||||
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, computed, graphql, queries, rest, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, computed, graphql, queries, rest, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.achievements))
|
||||
if ((!enabled) || (!q.achievements) || (!imports.metadata.plugins.achievements.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -37,7 +37,7 @@ export default async function({login, q, imports, data, computed, graphql, queri
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ inputs:
|
||||
Enable achievements plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_achievements_threshold:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false, markdown = "inline"} = {}) {
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false, markdown = "inline", extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.activity))
|
||||
if ((!enabled) || (!q.activity) || (!imports.metadata.plugins.activity.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Context
|
||||
@@ -174,6 +174,6 @@ export default async function({login, data, rest, q, account, imports}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, queries, imports, q, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, queries, imports, q, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.anilist))
|
||||
if ((!enabled) || (!q.anilist) || (!imports.metadata.plugins.anilist.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -122,14 +122,7 @@ export default async function({login, data, queries, imports, q, account}, {enab
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
console.debug(error.response.data)
|
||||
message = `API returned ${status}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ inputs:
|
||||
Enable aniList plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_anilist_user:
|
||||
type: string
|
||||
|
||||
@@ -8,7 +8,6 @@ export default async function({login, graphql, rest, data, q, queries, imports,
|
||||
//Load inputs
|
||||
console.debug(`metrics/compute/${login}/base > started`)
|
||||
let {indepth, hireable, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
|
||||
const extras = conf.settings.extras?.features ?? conf.settings.extras?.default
|
||||
const repositories = conf.settings.repositories || 100
|
||||
const forks = _forks ? "" : ", isFork: false"
|
||||
const affiliations = _affiliations?.length ? `, ownerAffiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]${conf.authenticated === login ? `, affiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]` : ""}` : ""
|
||||
@@ -90,7 +89,7 @@ export default async function({login, graphql, rest, data, q, queries, imports,
|
||||
}
|
||||
//Query contributions collection over account lifetime instead of last year
|
||||
if (account === "user") {
|
||||
if ((indepth) && (extras)) {
|
||||
if ((indepth) && (imports.metadata.plugins.base.extras("indepth", {...conf.settings, error:false}))) {
|
||||
const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"]
|
||||
const start = new Date(data.user.createdAt)
|
||||
const end = new Date()
|
||||
|
||||
@@ -49,7 +49,6 @@ inputs:
|
||||
default: no
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- plugins.base.indepth
|
||||
|
||||
base_hireable:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, data, imports, graphql, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, data, imports, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.calendar))
|
||||
if ((!enabled) || (!q.calendar) || (!imports.metadata.plugins.calendar.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -52,6 +52,6 @@ export default async function({login, q, data, imports, graphql, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, rest, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, rest, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.code))
|
||||
if ((!enabled) || (!q.code) || (!imports.metadata.plugins.code.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Context
|
||||
@@ -79,6 +79,6 @@ export default async function({login, q, imports, data, rest, account}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,8 @@ export default async function(
|
||||
},
|
||||
//Settings and tokens
|
||||
{
|
||||
enabled = false
|
||||
enabled = false,
|
||||
extras = false,
|
||||
} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({q, data, imports, account}, {enabled = false} = {}) {
|
||||
export default async function({q, data, imports, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.fortune))
|
||||
if ((!enabled) || (!q.fortune) || (!imports.metadata.plugins.fortune.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -40,6 +40,6 @@ export default async function({q, data, imports, account}, {enabled = false} = {
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
//Setup
|
||||
export default async function({q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.nightscout))
|
||||
if ((!enabled) || (!q.nightscout) || (!imports.metadata.plugins.nightscout.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {url, datapoints, lowalert, highalert, urgentlowalert, urgenthighalert} = imports.metadata.plugins.nightscout.inputs({data, account, q})
|
||||
|
||||
if (!url || url === "https://example.herokuapp.com")
|
||||
throw {error: {message: "Nightscout site URL isn't set!"}}
|
||||
throw {error: {message: "Nightscout URL is not set"}}
|
||||
if (url.substring(url.length - 1) !== "/")
|
||||
url += "/"
|
||||
if (url.substring(0, 7) === "http://")
|
||||
@@ -48,9 +48,7 @@ export default async function({q, imports, data, account}, {enabled = false} = {
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({q, imports, data, account}, {enabled = false, token = ""} = {}) {
|
||||
export default async function({q, imports, data, account}, {enabled = false, token = "", extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.poopmap))
|
||||
if ((!enabled) || (!q.poopmap) || (!imports.metadata.plugins.poopmap.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
if (!token)
|
||||
@@ -32,6 +32,6 @@ export default async function({q, imports, data, account}, {enabled = false, tok
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.screenshot))
|
||||
if ((!enabled) || (!q.screenshot) || (!imports.metadata.plugins.screenshot.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {url, selector, title, background} = imports.metadata.plugins.screenshot.inputs({data, account, q})
|
||||
if (!url)
|
||||
throw {error: {message: "An url is required"}}
|
||||
throw {error: {message: "URL is not set"}}
|
||||
|
||||
//Start puppeteer and navigate to page
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > starting browser`)
|
||||
@@ -37,8 +37,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {title: "Screenshot error", error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error, {title:"Screenshot error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ inputs:
|
||||
Enable screenshot plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_screenshot_title:
|
||||
description: |
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false, token} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stock))
|
||||
if ((!enabled) || (!q.stock) || (!imports.metadata.plugins.stock.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q})
|
||||
if (!token)
|
||||
throw {error: {message: "A token is required"}}
|
||||
throw {error: {message: "API token is not set"}}
|
||||
if (!symbol)
|
||||
throw {error: {message: "A company stock symbol is required"}}
|
||||
throw {error: {message: "Company stock symbol is not set"}}
|
||||
symbol = symbol.toLocaleUpperCase()
|
||||
|
||||
//Query API for company informations
|
||||
@@ -48,13 +48,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.contributors))
|
||||
if ((!enabled) || (!q.contributors) || (!imports.metadata.plugins.contributors.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -70,7 +70,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
|
||||
|
||||
//Contributions categories
|
||||
const types = Object.fromEntries([...new Set(Object.keys(categories))].map(type => [type, new Set()]))
|
||||
if ((sections.includes("categories")) && (extras)) {
|
||||
if ((sections.includes("categories")) && (imports.metadata.plugins.contributors.extras("categories", {extras}))) {
|
||||
//Temporary directory
|
||||
const repository = `${repo.owner}/${repo.repo}`
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${repository.replace(/[^\w]/g, "_")}`)
|
||||
@@ -123,6 +123,6 @@ export default async function({login, q, imports, data, rest, graphql, queries,
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ inputs:
|
||||
format: comma-separated
|
||||
default: ""
|
||||
extras:
|
||||
- metrics.run.setup.community.templates
|
||||
- metrics.setup.community.templates
|
||||
|
||||
template:
|
||||
description: |
|
||||
@@ -185,7 +185,7 @@ inputs:
|
||||
type: string
|
||||
default: ""
|
||||
extras:
|
||||
- metrics.run.user.css
|
||||
- metrics.run.puppeteer.user.css
|
||||
|
||||
extras_js:
|
||||
description: |
|
||||
@@ -199,7 +199,7 @@ inputs:
|
||||
type: string
|
||||
default: ""
|
||||
extras:
|
||||
- metrics.run.user.js
|
||||
- metrics.run.puppeteer.user.js
|
||||
|
||||
config_timezone:
|
||||
description: |
|
||||
@@ -347,6 +347,8 @@ inputs:
|
||||
default: ""
|
||||
preset: no
|
||||
example: "@lunar-red"
|
||||
extras:
|
||||
- metrics.setup.community.presets
|
||||
|
||||
retries:
|
||||
description: |
|
||||
@@ -497,6 +499,8 @@ inputs:
|
||||
default: no
|
||||
testing: yes
|
||||
preset: no
|
||||
extras:
|
||||
- metrics.npm.optional.libxml2
|
||||
|
||||
debug_flags:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.discussions))
|
||||
if ((!enabled) || (!q.discussions) || (!imports.metadata.plugins.discussions.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -64,6 +64,6 @@ export default async function({login, q, imports, graphql, queries, data, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async function({login, data, computed, imports, q, graphql, queri
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.followup))
|
||||
if ((!enabled) || (!q.followup) || (!imports.metadata.plugins.followup.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -56,10 +56,8 @@ export default async function({login, data, computed, imports, q, graphql, queri
|
||||
},
|
||||
}
|
||||
|
||||
//Extras features
|
||||
if (extras) {
|
||||
//Indepth mode
|
||||
if (indepth) {
|
||||
if ((indepth)&&(imports.metadata.plugins.followup.extras("indepth", {extras}))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > followup > indepth`)
|
||||
followup.indepth = {repositories: {}}
|
||||
|
||||
@@ -88,7 +86,6 @@ export default async function({login, data, computed, imports, q, graphql, queri
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Load user issues and pull requests
|
||||
if ((account === "user") && (sections.includes("user"))) {
|
||||
@@ -120,6 +117,6 @@ export default async function({login, data, computed, imports, q, graphql, queri
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.gists))
|
||||
if ((!enabled) || (!q.gists) || (!imports.metadata.plugins.gists.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -45,8 +45,6 @@ export default async function({login, data, graphql, q, imports, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.habits))
|
||||
if ((!enabled) || (!q.habits) || (!imports.metadata.plugins.habits.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -97,7 +97,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
|
||||
}
|
||||
|
||||
//Linguist
|
||||
if ((extras) && (charts)) {
|
||||
if ((charts)&&((imports.metadata.plugins.habits.extras("charts", {extras, error:false})))) {
|
||||
//Check if linguist exists
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
|
||||
if (patches.length) {
|
||||
@@ -113,7 +113,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
|
||||
}
|
||||
|
||||
//Generating charts with chartist
|
||||
if (_charts === "chartist") {
|
||||
if ((_charts === "chartist")&&(imports.metadata.plugins.habits.extras("charts.type", {extras}))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > generating charts`)
|
||||
habits.charts = await Promise.all([
|
||||
{type: "line", data: {...empty(24), ...Object.fromEntries(Object.entries(habits.commits.hours).filter(([k]) => !Number.isNaN(+k)))}, low: 0, high: habits.commits.hours.max},
|
||||
@@ -164,9 +164,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.introduction))
|
||||
if ((!enabled) || (!q.introduction) || (!imports.metadata.plugins.introduction.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -26,6 +26,6 @@ export default async function({login, q, imports, data, graphql, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.isocalendar))
|
||||
if ((!enabled) || (!q.isocalendar) || (!imports.metadata.plugins.isocalendar.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -72,9 +72,7 @@ export default async function({login, data, graphql, q, imports, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.languages))
|
||||
if ((!enabled) || (!q.languages) || (!imports.metadata.plugins.languages.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Context
|
||||
@@ -63,10 +63,8 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
||||
}
|
||||
}
|
||||
|
||||
//Extras features
|
||||
if (extras) {
|
||||
//Recently used languages
|
||||
if ((sections.includes("recently-used")) && (context.mode === "user")) {
|
||||
if ((sections.includes("recently-used")) && (context.mode === "user") && (imports.metadata.plugins.languages.extras("indepth", {extras}))) {
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > using recent analyzer`)
|
||||
languages["stats.recent"] = await recent_analyzer({login, data, imports, rest, account}, {skipped, categories: _recent_categories ?? categories, days: _recent_days, load: _recent_load, timeout})
|
||||
@@ -78,7 +76,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
||||
}
|
||||
|
||||
//Indepth mode
|
||||
if (indepth) {
|
||||
if ((indepth)&&(imports.metadata.plugins.languages.extras("indepth", {extras}))) {
|
||||
//Fetch gpg keys (web-flow is GitHub's public key when making changes from web ui)
|
||||
const gpg = []
|
||||
try {
|
||||
@@ -109,7 +107,6 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Apply aliases and group languages when needed
|
||||
for (const stats of [languages.stats, languages.lines, languages["stats.recent"].stats, languages["stats.recent"].lines]) {
|
||||
@@ -158,6 +155,6 @@ export default async function({login, data, imports, q, rest, account}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ inputs:
|
||||
type: boolean
|
||||
default: false
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.cpu.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
@@ -124,10 +124,6 @@ inputs:
|
||||
default: 15
|
||||
min: 1
|
||||
max: 30
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
plugin_languages_categories:
|
||||
description: |
|
||||
@@ -140,10 +136,6 @@ inputs:
|
||||
- programming
|
||||
- prose
|
||||
default: markup, programming
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
plugin_languages_recent_categories:
|
||||
description: |
|
||||
@@ -156,10 +148,6 @@ inputs:
|
||||
- programming
|
||||
- prose
|
||||
default: markup, programming
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
plugin_languages_recent_load:
|
||||
description: |
|
||||
@@ -168,10 +156,6 @@ inputs:
|
||||
default: 300
|
||||
min: 100
|
||||
max: 1000
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
plugin_languages_recent_days:
|
||||
description: |
|
||||
@@ -181,7 +165,3 @@ inputs:
|
||||
min: 0
|
||||
max: 365
|
||||
zero: disable
|
||||
extras:
|
||||
- metrics.api.github.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async function({login, q, imports, data, graphql, queries, accoun
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!extras) || (!q.licenses))
|
||||
if ((!enabled) || (!q.licenses) || (!imports.metadata.plugins.licenses.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -113,7 +113,7 @@ export default async function({login, q, imports, data, graphql, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ inputs:
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.cpu.overuse
|
||||
- metrics.run.tempdir
|
||||
- metrics.run.git
|
||||
- metrics.run.licensed
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, rest, q, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, imports, rest, q, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.lines))
|
||||
if ((!enabled) || (!q.lines) || (!imports.metadata.plugins.lines.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -42,6 +42,6 @@ export default async function({login, data, imports, rest, q, account}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ const modes = {
|
||||
}
|
||||
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = "", sandbox = false} = {}) {
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = "", sandbox = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.music))
|
||||
if ((!enabled) || (!q.music) || (!imports.metadata.plugins.music.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Initialization
|
||||
@@ -71,16 +71,16 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Provider
|
||||
if (!(provider in providers))
|
||||
throw {error: {message: provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw}
|
||||
throw {error: {message: provider ? `Unsupported provider "${provider}"` : "Provider is not set"}, ...raw}
|
||||
//Mode
|
||||
if (!(mode in modes))
|
||||
throw {error: {message: `Unsupported mode "${mode}"`}, ...raw}
|
||||
//Playlist mode
|
||||
if (mode === "playlist") {
|
||||
if (!playlist)
|
||||
throw {error: {message: "Missing playlist url"}, ...raw}
|
||||
throw {error: {message: "Playlist URL is not set"}, ...raw}
|
||||
if (!providers[provider].embed.test(playlist))
|
||||
throw {error: {message: "Unsupported playlist url format"}, ...raw}
|
||||
throw {error: {message: "Unsupported playlist URL format"}, ...raw}
|
||||
}
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
@@ -177,7 +177,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id) || (!client_secret) || (!refresh_token))
|
||||
throw {error: {message: "Spotify token must contain client id/secret and refresh token"}}
|
||||
throw {error: {message: "Token must contain client id, client secret and refresh token"}}
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
@@ -309,14 +309,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error: {message, instance: error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -364,9 +357,9 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id) || (!client_secret) || (!refresh_token))
|
||||
throw {error: {message: "Spotify token must contain client id/secret and refresh token"}}
|
||||
throw {error: {message: "Token must contain client id, client secret and refresh token"}}
|
||||
else if (limit > 50)
|
||||
throw {error: {message: "Spotify top limit cannot be greater than 50"}}
|
||||
throw {error: {message: "Top limit cannot exceed 50 for this provider"}}
|
||||
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
@@ -422,14 +415,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error: {message, instance: error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -473,14 +459,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.message ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error: {message, instance: error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -513,13 +492,11 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error: {message: "An error occured (could not retrieve tracks)"}}
|
||||
throw {error: {message: "Failed to retrieve tracks"}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ inputs:
|
||||
Enable music plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_music_provider:
|
||||
description: |
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async function({login, q, imports, rest, graphql, data, account,
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.notable))
|
||||
if ((!enabled) || (!q.notable) || (!imports.metadata.plugins.notable.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -32,10 +32,8 @@ export default async function({login, q, imports, rest, graphql, data, account,
|
||||
contributions = await Promise.all(contributions.map(async ({handle, stars, issues, pulls, avatarUrl, organization}) => ({name: handle.split("/").shift(), handle, stars, issues, pulls, avatar: await imports.imgb64(avatarUrl), organization})))
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > found ${contributions.length} notable contributions`)
|
||||
|
||||
//Extras features
|
||||
if (extras) {
|
||||
//Indepth
|
||||
if (indepth) {
|
||||
if ((indepth)&&(imports.metadata.plugins.notable.extras("indepth", {extras}))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > indepth`)
|
||||
|
||||
//Fetch issues
|
||||
@@ -103,7 +101,6 @@ export default async function({login, q, imports, rest, graphql, data, account,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Aggregate contributions
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > aggregating results`)
|
||||
@@ -142,6 +139,6 @@ export default async function({login, q, imports, rest, graphql, data, account,
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = null} = {}) {
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = null, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.pagespeed) || ((!data.user.websiteUrl) && (!q["pagespeed.url"])))
|
||||
if ((!enabled) || (!q.pagespeed) || (!imports.metadata.plugins.pagespeed.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {detailed, screenshot, url, pwa} = imports.metadata.plugins.pagespeed.inputs({data, account, q})
|
||||
//Format url if needed
|
||||
if (!url)
|
||||
throw {error: {message: "Website URL is not set"}}
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const {protocol, host} = imports.url.parse(url)
|
||||
@@ -45,15 +47,13 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
throw imports.format.error(error, {descriptions:{"429":'(consider using "plugin_pagespeed_token")', custom(error) {
|
||||
const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
if (description) {
|
||||
const status = error.response?.status
|
||||
let description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
if ((status === 429) && (!description))
|
||||
description = 'consider using "plugin_pagespeed_token"'
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
return `API error: ${status} (${description})`
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
return null
|
||||
}}})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, rest, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, graphql, rest, q, queries, imports, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.people))
|
||||
if ((!enabled) || (!q.people) || (!imports.metadata.plugins.people.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Context
|
||||
@@ -102,6 +102,6 @@ export default async function({login, data, graphql, rest, q, queries, imports,
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, imports, q, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.posts))
|
||||
if ((!enabled) || (!q.posts) || (!imports.metadata.plugins.posts.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -51,12 +51,10 @@ export default async function({login, data, imports, q, queries, account}, {enab
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error: {message: "An error occured (could not retrieve posts)"}}
|
||||
throw {error: {message: "Failed to retrieve posts"}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, imports, graphql, q, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.projects))
|
||||
if ((!enabled) || (!q.projects) || (!imports.metadata.plugins.projects.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -66,9 +66,10 @@ export default async function({login, data, imports, graphql, q, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
throw imports.format.error(error, {descriptions:{custom(error) {
|
||||
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
|
||||
message = "Insufficient token rights"
|
||||
throw {error: {message, instance: error}}
|
||||
return "Insufficient token scopes"
|
||||
return null
|
||||
}}})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.reactions))
|
||||
if ((!enabled) || (!q.reactions) || (!imports.metadata.plugins.reactions.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -60,6 +60,6 @@ export default async function({login, q, imports, data, graphql, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.repositories))
|
||||
if ((!enabled) || (!q.repositories) || (!imports.metadata.plugins.repositories.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -30,7 +30,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.rss))
|
||||
if ((!enabled) || (!q.rss) || (!imports.metadata.plugins.rss.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {source, limit} = imports.metadata.plugins.rss.inputs({data, account, q})
|
||||
if (!source)
|
||||
throw {error: {message: "A RSS feed is required"}}
|
||||
throw {error: {message: "RSS feed URL is not set"}}
|
||||
|
||||
//Load rss feed
|
||||
const {title, description, link, items} = await (new imports.rss()).parseURL(source) //eslint-disable-line new-cap
|
||||
@@ -26,8 +26,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.skyline))
|
||||
if ((!enabled) || (!q.skyline) || (!imports.metadata.plugins.skyline.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -42,6 +42,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ inputs:
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.cpu.overuse
|
||||
- metrics.npm.optional.gifencoder
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_skyline_year:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.sponsors))
|
||||
if ((!enabled) || (!q.sponsors) || (!imports.metadata.plugins.sponsors.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -85,6 +85,6 @@ export default async function({login, q, imports, data, graphql, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stackoverflow))
|
||||
if ((!enabled) || (!q.stackoverflow) || (!imports.metadata.plugins.stackoverflow.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {sections, user, limit, lines, "lines.snippet": codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
if (!user)
|
||||
throw {error: {message: "You must provide a stackoverflow user id"}}
|
||||
throw {error: {message: "Stack Overflow user id is not set"}}
|
||||
|
||||
//Initialization
|
||||
//See https://api.stackexchange.com/docs
|
||||
@@ -64,9 +64,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stargazers))
|
||||
if ((!enabled) || (!q.stargazers) || (!imports.metadata.plugins.stargazers.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -59,7 +59,7 @@ export default async function({login, graphql, data, imports, q, queries, accoun
|
||||
|
||||
//Generating charts
|
||||
let charts = null
|
||||
if (_charts === "chartist") {
|
||||
if ((_charts === "chartist")&&(imports.metadata.plugins.stargazers.extras("charts.type", {extras}))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > generating charts`)
|
||||
charts = await Promise.all([{data: total, low: total.min, high: total.max}, {data: increments, ref: 0, low: increments.min, high: increments.max, sign: true}].map(({data: {dates: set}, high, low, ref, sign = false}) =>
|
||||
imports.chartist("line", {
|
||||
@@ -100,6 +100,6 @@ export default async function({login, graphql, data, imports, q, queries, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.starlists))
|
||||
if ((!enabled) || (!q.starlists) || (!imports.metadata.plugins.starlists.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -101,6 +101,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ inputs:
|
||||
Enable starlists plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_starlists_limit:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, graphql, q, queries, imports, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stars))
|
||||
if ((!enabled) || (!q.stars) || (!imports.metadata.plugins.stars.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -30,8 +30,6 @@ export default async function({login, data, graphql, q, queries, imports, accoun
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.support))
|
||||
if ((!enabled) || (!q.support) || (!imports.metadata.plugins.support.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -24,7 +24,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
await frame.waitForSelector(".user-profile-names", {timeout: 5000})
|
||||
}
|
||||
catch {
|
||||
throw {error: {message: "Could not find matching account on github.community"}}
|
||||
throw {error: {message: "Account does not exists on github.community"}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ inputs:
|
||||
Enable support plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
export default async function({login, data, imports, q, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.topics))
|
||||
if ((!enabled) || (!q.topics) || (!imports.metadata.plugins.topics.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -88,8 +88,6 @@ export default async function({login, data, imports, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error: {message: "An error occured", instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ inputs:
|
||||
Enable topics plugin
|
||||
type: boolean
|
||||
default: no
|
||||
extras:
|
||||
- metrics.run.puppeteer.scrapping
|
||||
|
||||
plugin_topics_mode:
|
||||
description: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, rest, q, account}, {enabled = false} = {}) {
|
||||
export default async function({login, imports, data, rest, q, account}, {enabled = false, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.traffic))
|
||||
if ((!enabled) || (!q.traffic) || (!imports.metadata.plugins.traffic.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -27,9 +27,6 @@ export default async function({login, imports, data, rest, q, account}, {enabled
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.status === 403)
|
||||
message = "Insufficient token rights"
|
||||
throw {error: {message, instance: error}}
|
||||
throw imports.format.error(error, {descriptions:{"403":"Insufficient token scopes"}})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = "", extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.tweets))
|
||||
if ((!enabled) || (!q.tweets) || (!imports.metadata.plugins.tweets.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -93,13 +93,6 @@ export default async function({login, imports, data, q, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token, extras = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if (!enabled || !q.wakatime)
|
||||
if ((!enabled) || (!q.wakatime) || (!imports.metadata.plugins.wakatime.extras("enabled", {extras})))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
@@ -46,13 +46,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
message = `API returned ${status}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error: {message, instance: error}}
|
||||
throw imports.format.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
{"source": "/:login([-\\w]+)/:repository([-\\w]+)", "destination": "https://metrics.lecoq.io/:login/:repository"},
|
||||
{"source": "/about/query/:login", "destination": "https://metrics.lecoq.io/about/query/:login"},
|
||||
{"source": "/about/query/:login/:plugin", "destination": "https://metrics.lecoq.io/about/query/:login/:plugin"},
|
||||
{"source": "/insights/query/:login", "destination": "https://metrics.lecoq.io/insights/query/:login"},
|
||||
{"source": "/insights/query/:login/:plugin", "destination": "https://metrics.lecoq.io/insights/query/:login/:plugin"},
|
||||
{"source": "/.uncache", "destination": "https://metrics.lecoq.io/.uncache"},
|
||||
{"source": "/.requests", "destination": "https://metrics.lecoq.io/.requests"}
|
||||
],
|
||||
|
||||