From b5a79253151b92c0f48c7448d77476af14d5b9c0 Mon Sep 17 00:00:00 2001
From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com>
Date: Fri, 22 Oct 2021 12:51:50 -0400
Subject: [PATCH] feat(core): add `insights` output (#575)
---
source/app/action/index.mjs | 21 +++++++-
source/app/metrics/index.mjs | 66 ++++++++++++++++++++++++++
source/app/metrics/setup.mjs | 2 +-
source/app/web/instance.mjs | 31 +-----------
source/app/web/statics/about/script.js | 11 ++++-
source/plugins/core/README.md | 15 ++++++
source/plugins/core/index.mjs | 2 +-
source/plugins/core/metadata.yml | 13 ++---
8 files changed, 120 insertions(+), 41 deletions(-)
diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs
index 00c02d3a..1dd59940 100644
--- a/source/app/action/index.mjs
+++ b/source/app/action/index.mjs
@@ -5,6 +5,7 @@ import octokit from "@octokit/graphql"
import fs from "fs/promises"
import paths from "path"
import sgit from "simple-git"
+import processes from "child_process"
import metrics from "../metrics/index.mjs"
import setup from "../metrics/setup.mjs"
import mocks from "../mocks/index.mjs"
@@ -96,8 +97,8 @@ async function wait(seconds) {
...config
} = metadata.plugins.core.inputs.action({core})
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
- const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null
- const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf"}[_output] ?? _output)
+ const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template].formats[0] ?? null
+ const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf", insights:"html"}[_output] ?? _output)
//Docker image
if (_image)
@@ -248,6 +249,22 @@ async function wait(seconds) {
Object.assign(q, config)
if (/markdown/.test(convert))
info("Markdown cache", _markdown_cache)
+ if (/insights/.test(convert)) {
+ try {
+ await new Promise(async (solve, reject) => {
+ let stdout = ""
+ setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5*60*1000)
+ const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env, NO_SETTINGS: true }})
+ web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null))
+ web.stderr.on("data", data => console.debug(`web > ${data}`))
+ })
+ info("Insights webserver", "ok")
+ }
+ catch (error) {
+ info("Insights webserver", "(failed to initialize)")
+ throw error
+ }
+ }
//Base content
info.break()
diff --git a/source/app/metrics/index.mjs b/source/app/metrics/index.mjs
index 92aff4b4..84e95b84 100644
--- a/source/app/metrics/index.mjs
+++ b/source/app/metrics/index.mjs
@@ -45,6 +45,10 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
if (conf.settings["debug.headless"])
imports.puppeteer.headless = false
+ //Metrics insights
+ if (convert === "insights")
+ return metrics.insights.output({login, imports, conf}, {graphql, rest, Plugins, Templates})
+
//Partial parts
{
data.partials = new Set([
@@ -209,3 +213,65 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
throw error
}
}
+
+//Metrics insights
+metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) {
+ const q = {
+ template:"classic",
+ achievements:true,
+ "achievements.threshold":"X",
+ isocalendar:true,
+ "isocalendar.duration":"full-year",
+ languages:true,
+ "languages.limit":0,
+ activity:true,
+ "activity.limit":100,
+ "activity.days":0,
+ notable:true,
+ followup:true,
+ "followup.sections":"repositories, user",
+ habits:true,
+ "habits.from":100,
+ "habits.days":7,
+ "habits.facts":false,
+ "habits.charts":true,
+ introduction:true
+ }
+ const plugins = {achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true, extras:false}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true, extras:false}, introduction:{enabled:true}}
+ return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates})
+}
+
+//Metrics insights static render
+metrics.insights.output = async function ({login, imports, conf}, {graphql, rest, Plugins, Templates}) {
+ //Server
+ console.debug(`metrics/compute/${login} > insights`)
+ const server = `http://localhost:${conf.settings.port}`
+ console.debug(`metrics/compute/${login} > insights > server on port ${conf.settings.port}`)
+
+ //Data processing
+ const browser = await imports.puppeteer.launch()
+ const page = await browser.newPage()
+ console.debug(`metrics/compute/${login} > insights > generating data`)
+ const json = JSON.stringify(await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates}))
+ await page.goto(`${server}/about/${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.waitForSelector(".container .user", {timeout:10*60*1000})
+
+ //Rendering
+ console.debug(`metrics/compute/${login} > insights > rendering data`)
+ const rendered = `
+
+
+
+ Metrics insights: ${login}
+
+
+
+ ${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}) => ``).join("\n")}
+
+ `
+ await browser.close()
+ return {mime:"text/html", rendered}
+}
\ No newline at end of file
diff --git a/source/app/metrics/setup.mjs b/source/app/metrics/setup.mjs
index d561866d..31280be7 100644
--- a/source/app/metrics/setup.mjs
+++ b/source/app/metrics/setup.mjs
@@ -30,7 +30,7 @@ export default async function({log = true, nosettings = false, community = {}} =
authenticated:null,
templates:{},
queries:{},
- settings:{},
+ settings:{port:3000},
metadata:{},
paths:{
statics:__statics,
diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs
index 395e7ca2..4e5552ae 100644
--- a/source/app/web/instance.mjs
+++ b/source/app/web/instance.mjs
@@ -174,34 +174,7 @@ export default async function({mock, nosettings} = {}) {
}
//Compute metrics
console.debug(`metrics/app/${login}/insights > compute insights`)
- const json = await metrics(
- {
- login,
- q:{
- template:"classic",
- achievements:true,
- "achievements.threshold":"X",
- isocalendar:true,
- "isocalendar.duration":"full-year",
- languages:true,
- "languages.limit":0,
- activity:true,
- "activity.limit":100,
- "activity.days":0,
- notable:true,
- followup:true,
- "followup.sections":"repositories, user",
- habits:true,
- "habits.from":100,
- "habits.days":7,
- "habits.facts":false,
- "habits.charts":true,
- introduction:true
- },
- },
- {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true}, introduction:{enabled:true}}, conf, convert:"json"},
- {Plugins, Templates},
- )
+ const json = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates})
//Cache
if ((!debug) && (cached)) {
const maxage = Math.round(Number(req.query.cache))
@@ -286,7 +259,7 @@ export default async function({mock, nosettings} = {}) {
conf,
die:q["plugins.errors.fatal"] ?? false,
verify:q.verify ?? false,
- convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
+ convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
}, {Plugins, Templates})
//Cache
if ((!debug) && (cached)) {
diff --git a/source/app/web/statics/about/script.js b/source/app/web/statics/about/script.js
index 0a181cc2..8895b238 100644
--- a/source/app/web/statics/about/script.js
+++ b/source/app/web/statics/about/script.js
@@ -9,6 +9,10 @@
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
}
catch (error) {}
+ //Embed
+ this.embed = !!(new URLSearchParams(location.search).get("embed"))
+ //From local storage
+ this.localstorage = !!(new URLSearchParams(location.search).get("localstorage"))
//User
const user = location.pathname.split("/").pop()
if ((user) && (user !== "about")) {
@@ -18,8 +22,6 @@
else {
this.searchable = true
}
- //Embed
- this.embed = !!(new URLSearchParams(location.search).get("embed"))
//Init
await Promise.all([
//GitHub limit tracker
@@ -80,6 +82,10 @@
this.error = null
this.metrics = null
this.pending = true
+ if (this.localstorage) {
+ this.metrics = JSON.parse(localStorage.getItem("local.metrics") ?? "null")
+ return
+ }
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
}
catch (error) {
@@ -143,6 +149,7 @@
hosted: null,
user: "",
embed: false,
+ localstorage: false,
searchable: false,
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
palette: "light",
diff --git a/source/plugins/core/README.md b/source/plugins/core/README.md
index 0879308a..b8b189c4 100644
--- a/source/plugins/core/README.md
+++ b/source/plugins/core/README.md
@@ -272,6 +272,21 @@ It is possible to convert output to PDF when using a markdown template by settin
config_output: markdown-pdf
```
+### ✨ Render `Metrics insights` statically
+
+It is possible to generate an HTML file containing `✨ Metrics insights` output by setting `config_output` to `insights`. Resulting output will already be pre-rendered and not contain any external sources (i.e. no JavaScript and style sheets).
+
+> Note that like `✨ Metrics insights` content is not configurable.
+
+#### ℹ️ Examples workflows
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ config_output: insights
+```
+
### 🐳 Faster execution with prebuilt docker images
If you're using the official release `lowlighter/metrics` as a GitHub Action (either a specific version, `@latest` or `@master`), it'll pull a prebuilt docker container image from [GitHub Container Registry](https://github.com/users/lowlighter/packages/container/package/metrics) which contains already installed dependencies which will cut execution time from ~5 minutes to ~1 minute.
diff --git a/source/plugins/core/index.mjs b/source/plugins/core/index.mjs
index 7985d545..dd9e69b5 100644
--- a/source/plugins/core/index.mjs
+++ b/source/plugins/core/index.mjs
@@ -47,7 +47,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
pending.push((async () => {
try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
- data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {...plugins[name], extras:conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false})
+ data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {extras:conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false, ...plugins[name]})
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
}
catch (error) {
diff --git a/source/plugins/core/metadata.yml b/source/plugins/core/metadata.yml
index 08421585..33ce4854 100644
--- a/source/plugins/core/metadata.yml
+++ b/source/plugins/core/metadata.yml
@@ -197,13 +197,14 @@ inputs:
type: string
default: auto
values:
- - auto # Defaults to template default
+ - auto # Defaults to template default
- svg
- - png # Does not support animations
- - jpeg # Does not support animations and transparency
- - json # Outputs a JSON file instead of an image
- - markdown # Outputs a Markdown file instead of an image
- - markdown-pdf # Outputs a Markdown file as PDF instead of an image
+ - png # Does not support animations
+ - jpeg # Does not support animations and transparency
+ - json # Outputs a JSON file instead of an image
+ - markdown # Outputs a Markdown file instead of an image
+ - markdown-pdf # Outputs a Markdown file as PDF instead of an image
+ - insights # Outputs a rendered HTML file of Metrics Insights
# Number of retries in case rendering fail
retries: