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: