Improve web instance (#149)
This commit is contained in:
16
.github/readme/partials/setup/web.md
vendored
16
.github/readme/partials/setup/web.md
vendored
@@ -101,22 +101,6 @@ systemctl status github_metrics
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>⚠️ HTTP errors code</summary>
|
|
||||||
|
|
||||||
Following error codes may be encountered on web instance:
|
|
||||||
|
|
||||||
| Error code | Description |
|
|
||||||
| ------------------------- | -------------------------------------------------------------------------- |
|
|
||||||
| `400 Bad request` | Invalid query (e.g. unsupported template) |
|
|
||||||
| `403 Forbidden` | User not allowed in `restricted` users list |
|
|
||||||
| `404 Not found` | GitHub API did not found the requested user |
|
|
||||||
| `429 Too many requests` | Thrown when rate limiter is trigerred |
|
|
||||||
| `500 Internal error` | Server error while generating metrics images (check logs for more details) |
|
|
||||||
| `503 Service unavailable` | Maximum user capacity reached, only cached images can be accessed for now |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>🔗 HTTP parameters</summary>
|
<summary>🔗 HTTP parameters</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
skip(req, _res) {
|
skip(req, _res) {
|
||||||
return !!cache.get(req.params.login)
|
return !!cache.get(req.params.login)
|
||||||
},
|
},
|
||||||
message:"Too many requests",
|
message:"Too many requests: retry later",
|
||||||
|
headers:true,
|
||||||
...ratelimiter,
|
...ratelimiter,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -74,11 +75,11 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Base routes
|
//Base routes
|
||||||
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60*1000})
|
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60*1000, headers:false})
|
||||||
const metadata = Object.fromEntries(Object.entries(conf.metadata.plugins)
|
const metadata = Object.fromEntries(Object.entries(conf.metadata.plugins)
|
||||||
.filter(([key]) => !["base", "core"].includes(key))
|
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].includes(key)))])
|
||||||
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].includes(key)))]))
|
.map(([key, value]) => [key, key === "core" ? {...value, web:Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]))
|
||||||
const enabled = Object.entries(metadata).map(([name]) => ({name, enabled:plugins[name]?.enabled ?? false}))
|
const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, enabled:plugins[name]?.enabled ?? false}))
|
||||||
const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
|
const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
|
||||||
const actions = {flush:new Map()}
|
const actions = {flush:new Map()}
|
||||||
let requests = {limit:0, used:0, remaining:0, reset:NaN}
|
let requests = {limit:0, used:0, remaining:0, reset:NaN}
|
||||||
@@ -119,9 +120,10 @@
|
|||||||
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
|
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
|
||||||
//Meta
|
//Meta
|
||||||
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
||||||
app.get("/.requests", limiter, async(req, res) => res.status(200).json(requests))
|
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
|
||||||
|
app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null))
|
||||||
//Cache
|
//Cache
|
||||||
app.get("/.uncache", limiter, async(req, res) => {
|
app.get("/.uncache", limiter, (req, res) => {
|
||||||
const {token, user} = req.query
|
const {token, user} = req.query
|
||||||
if (token) {
|
if (token) {
|
||||||
if (actions.flush.has(token)) {
|
if (actions.flush.has(token)) {
|
||||||
@@ -129,7 +131,7 @@
|
|||||||
cache.del(actions.flush.get(token))
|
cache.del(actions.flush.get(token))
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
||||||
@@ -139,25 +141,32 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Metrics
|
//Metrics
|
||||||
|
const pending = new Set()
|
||||||
|
app.get("/:login/:repository", ...middlewares, (req, res) => res.redirect(`/${req.params.login}?template=repository&repo=${req.params.repository}&${Object.entries(req.query).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join("&")}`))
|
||||||
app.get("/:login", ...middlewares, async(req, res) => {
|
app.get("/:login", ...middlewares, async(req, res) => {
|
||||||
//Request params
|
//Request params
|
||||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||||
if ((restricted.length)&&(!restricted.includes(login))) {
|
if ((restricted.length)&&(!restricted.includes(login))) {
|
||||||
console.debug(`metrics/app/${login} > 403 (not in whitelisted users)`)
|
console.debug(`metrics/app/${login} > 403 (not in allowed users)`)
|
||||||
return res.sendStatus(403)
|
return res.status(403).send(`Forbidden: "${login}" not in allowed users`)
|
||||||
}
|
}
|
||||||
//Read cached data if possible
|
//Read cached data if possible
|
||||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||||
const {rendered, mime} = cache.get(login)
|
const {rendered, mime} = cache.get(login)
|
||||||
res.header("Content-Type", mime)
|
res.header("Content-Type", mime)
|
||||||
res.send(rendered)
|
return res.send(rendered)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
//Maximum simultaneous users
|
//Maximum simultaneous users
|
||||||
if ((maxusers)&&(cache.size()+1 > maxusers)) {
|
if ((maxusers)&&(cache.size()+1 > maxusers)) {
|
||||||
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
||||||
return res.sendStatus(503)
|
return res.status(503).send("Service Unavailable: maximum users reached, only cached metrics are available")
|
||||||
}
|
}
|
||||||
|
//Prevent multiples requests
|
||||||
|
if (pending.has(login)) {
|
||||||
|
console.debug(`metrics/app/${login} > 409 (multiple requests)`)
|
||||||
|
return res.status(409).send(`Conflict: a request for "${login}" is being process, retry later`)
|
||||||
|
}
|
||||||
|
pending.add(login)
|
||||||
|
|
||||||
//Compute rendering
|
//Compute rendering
|
||||||
try {
|
try {
|
||||||
@@ -175,23 +184,27 @@
|
|||||||
cache.put(login, {rendered, mime}, cached)
|
cache.put(login, {rendered, mime}, cached)
|
||||||
//Send response
|
//Send response
|
||||||
res.header("Content-Type", mime)
|
res.header("Content-Type", mime)
|
||||||
res.send(rendered)
|
return res.send(rendered)
|
||||||
}
|
}
|
||||||
//Internal error
|
//Internal error
|
||||||
catch (error) {
|
catch (error) {
|
||||||
//Not found user
|
//Not found user
|
||||||
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
|
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
|
||||||
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
||||||
return res.sendStatus(404)
|
return res.status(404).send(`Not found: unknown user or organization "${login}"`)
|
||||||
}
|
}
|
||||||
//Invalid template
|
//Invalid template
|
||||||
if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) {
|
if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) {
|
||||||
console.debug(`metrics/app/${login} > 400 (bad request)`)
|
console.debug(`metrics/app/${login} > 400 (bad request)`)
|
||||||
return res.sendStatus(400)
|
return res.status(400).send(`Bad request: unsupported template "${req.query.template}"`)
|
||||||
}
|
}
|
||||||
//General error
|
//General error
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.sendStatus(500)
|
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
|
||||||
|
}
|
||||||
|
//After rendering
|
||||||
|
finally {
|
||||||
|
pending.delete(login)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
"mocked": false, "//": "Use mocked data instead of live APIs (use 'force' to use mocked token even if real token are defined)",
|
"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",
|
"repositories": 100, "//": "Number of repositories to use",
|
||||||
"padding": ["6%", "15%"], "//": "Image padding (default)",
|
"padding": ["6%", "15%"], "//": "Image padding (default)",
|
||||||
|
"hosted": {
|
||||||
|
"by": "", "//": "Web instance host (displayed in footer)",
|
||||||
|
"link": "", "//": "Web instance host link (displayed in footer)"
|
||||||
|
},
|
||||||
"community": {
|
"community": {
|
||||||
"templates": [], "//": "Additional community templates to setup"
|
"templates": [], "//": "Additional community templates to setup"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
const {data:metadata} = await axios.get("/.plugins.metadata")
|
const {data:metadata} = await axios.get("/.plugins.metadata")
|
||||||
const {data:base} = await axios.get("/.plugins.base")
|
const {data:base} = await axios.get("/.plugins.base")
|
||||||
const {data:version} = await axios.get("/.version")
|
const {data:version} = await axios.get("/.version")
|
||||||
|
const {data:hosted} = await axios.get("/.hosted")
|
||||||
templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name))
|
templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name))
|
||||||
|
//Disable unsupported options
|
||||||
|
delete metadata.core.web.output
|
||||||
|
delete metadata.core.web.twemojis
|
||||||
//App
|
//App
|
||||||
return new Vue({
|
return new Vue({
|
||||||
//Initialization
|
//Initialization
|
||||||
@@ -52,10 +56,9 @@
|
|||||||
palette:"light",
|
palette:"light",
|
||||||
requests:{limit:0, used:0, remaining:0, reset:0},
|
requests:{limit:0, used:0, remaining:0, reset:0},
|
||||||
cached:new Map(),
|
cached:new Map(),
|
||||||
config:{
|
config:Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
|
||||||
timezone:"",
|
metadata:Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
|
||||||
animated:true,
|
hosted,
|
||||||
},
|
|
||||||
plugins:{
|
plugins:{
|
||||||
base,
|
base,
|
||||||
list:plugins,
|
list:plugins,
|
||||||
@@ -118,12 +121,14 @@
|
|||||||
.filter(([key, value]) => `${value}`.length)
|
.filter(([key, value]) => `${value}`.length)
|
||||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
.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
|
//Config
|
||||||
const config = Object.entries(this.config).filter(([key, value]) => value).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
const config = Object.entries(this.config).filter(([key, value]) => (value)&&(value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||||
//Template
|
//Template
|
||||||
const template = (this.templates.selected !== templates[0]) ? [`template=${this.templates.selected}`] : []
|
const template = (this.templates.selected !== templates[0]) ? [`template=${this.templates.selected}`] : []
|
||||||
//Generated url
|
//Generated url
|
||||||
const params = [...template, ...plugins, ...options, ...config].join("&")
|
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
|
||||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||||
},
|
},
|
||||||
//Embedded generated code
|
//Embedded generated code
|
||||||
@@ -157,9 +162,10 @@
|
|||||||
` template: ${this.templates.selected}`,
|
` template: ${this.templates.selected}`,
|
||||||
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ")||'""'}`,
|
` 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(/[.]/, "_")}: ${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.enabled).filter(([key, value]) => (key !== "base")&&(value)).map(([key]) => ` plugin_${key}: yes`),
|
||||||
...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
||||||
...Object.entries(this.config).filter(([key, value]) => value).map(([key, value]) => ` config_${key.replace(/[.]/, "_")}: ${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(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
||||||
].sort(),
|
].sort(),
|
||||||
].join("\n")
|
].join("\n")
|
||||||
},
|
},
|
||||||
@@ -185,7 +191,7 @@
|
|||||||
this.templates.placeholder.timeout = setTimeout(async () => {
|
this.templates.placeholder.timeout = setTimeout(async () => {
|
||||||
this.templates.placeholder.image = await placeholder(this)
|
this.templates.placeholder.image = await placeholder(this)
|
||||||
this.generated.content = ""
|
this.generated.content = ""
|
||||||
this.generated.error = false
|
this.generated.error = null
|
||||||
}, timeout)
|
}, timeout)
|
||||||
},
|
},
|
||||||
//Resize mock image
|
//Resize mock image
|
||||||
@@ -207,9 +213,9 @@
|
|||||||
try {
|
try {
|
||||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
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.content = (await axios.get(this.url)).data
|
||||||
this.generated.error = false
|
this.generated.error = null
|
||||||
} catch {
|
} catch (error) {
|
||||||
this.generated.error = true
|
this.generated.error = {code:error.response.status, message:error.response.data}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
this.generated.pending = false
|
this.generated.pending = false
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
const data = {
|
const data = {
|
||||||
//Template elements
|
//Template elements
|
||||||
style, fonts, errors:[],
|
style, fonts, errors:[],
|
||||||
partials:new Set(partials),
|
partials:new Set([...(set.config.order||"").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]),
|
||||||
//Plural helper
|
//Plural helper
|
||||||
s(value, end = "") {
|
s(value, end = "") {
|
||||||
return value !== 1 ? {y:"ies", "":"s"}[end] : end
|
return value !== 1 ? {y:"ies", "":"s"}[end] : end
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<main :class="[palette]">
|
<main :class="[palette]">
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<header>
|
<header v-once>
|
||||||
<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>
|
<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 v{{ version }}</a>
|
<a href="https://github.com/lowlighter/metrics">Metrics v{{ version }}</a>
|
||||||
</header>
|
</header>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
<div class="ui-avatar" :style="{backgroundImage:avatar ? `url(${avatar})` : 'none'}"></div>
|
<div class="ui-avatar" :style="{backgroundImage:avatar ? `url(${avatar})` : 'none'}"></div>
|
||||||
|
|
||||||
<input type="text" v-model="user" placeholder="Your GitHub username" @keyup="mock">
|
<input type="text" v-model="user" placeholder="Your GitHub username">
|
||||||
<button @click="generate" :disabled="(!user)||(generated.pending)">
|
<button @click="generate" :disabled="(!user)||(generated.pending)">
|
||||||
{{ generated.pending ? 'Working on it :)' : 'Generate your metrics!' }}
|
{{ generated.pending ? 'Working on it :)' : 'Generate your metrics!' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
|
|
||||||
<div class="configuration" v-if="configure">
|
<div class="configuration" v-if="configure">
|
||||||
<b>🔧 Configure plugins</b>
|
<b>🔧 Configure plugins</b>
|
||||||
|
|
||||||
<template v-for="(input, key) in configure">
|
<template v-for="(input, key) in configure">
|
||||||
<b v-if="typeof input === 'string'">{{ input }}</b>
|
<b v-if="typeof input === 'string'">{{ input }}</b>
|
||||||
<label v-else class="option">
|
<label v-else class="option">
|
||||||
@@ -90,7 +89,25 @@
|
|||||||
<input type="text" v-else v-model="plugins.options[key]" @change="mock" :placeholder="input.placeholder">
|
<input type="text" v-else v-model="plugins.options[key]" @change="mock" :placeholder="input.placeholder">
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</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 }}</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>
|
</div>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
@@ -103,7 +120,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="tab == 'overview'">
|
<div v-if="tab == 'overview'">
|
||||||
<div class="error" v-if="generated.error">An error occurred while generating your metrics :( Please try again later.</div>
|
<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 class="image" :class="{pending:generated.pending}" v-html="generated.content||templates.placeholder.image"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab == 'markdown'">
|
<div v-else-if="tab == 'markdown'">
|
||||||
@@ -122,6 +142,13 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer v-once>
|
||||||
|
<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/github-metrics-as-svg-image">GitHub Action</a>
|
||||||
|
<span v-if="hosted">Hosted with ❤️ by <a :href="hosted.link">{{ hosted.by }}</a></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
|
|||||||
@@ -232,6 +232,46 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Details */
|
||||||
|
details summary:hover {
|
||||||
|
background-color: var(--color-input-contrast-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
.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);
|
||||||
|
border: 1px solid var(--color-alert-error-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
main > footer {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
font-size: .8rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-top: 1px solid var(--color-border-secondary);
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
main > footer > * {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Media screen */
|
/* Media screen */
|
||||||
@media only screen and (min-width: 740px) {
|
@media only screen and (min-width: 740px) {
|
||||||
.ui {
|
.ui {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ inputs:
|
|||||||
type: array
|
type: array
|
||||||
format: comma-separated
|
format: comma-separated
|
||||||
default: ""
|
default: ""
|
||||||
|
example: base.header, base.repositories
|
||||||
|
|
||||||
# Use twemojis instead of emojis
|
# Use twemojis instead of emojis
|
||||||
# May increase filesize but emojis will be rendered the same across all platforms
|
# May increase filesize but emojis will be rendered the same across all platforms
|
||||||
|
|||||||
Reference in New Issue
Block a user