Version 2.10 (#30)

This commit is contained in:
Simon Lecoq
2020-12-28 20:04:44 +01:00
committed by GitHub
parent 8135bf19a6
commit 159b0757a8
39 changed files with 922 additions and 559 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
template: ["classic","terminal"] template: ["classic","repository","terminal"]
steps: steps:
- name: ${{ matrix.template }} > Base - name: ${{ matrix.template }} > Base
@@ -53,7 +53,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: header, activity, community, repositories, metadata base: header, activity, community, repositories, metadata
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -63,32 +63,21 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_pagespeed: yes plugin_pagespeed: yes
plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }} plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
- name: ${{ matrix.template }} > Plugin > PageSpeed (detailed)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_pagespeed: yes
plugin_pagespeed_detailed: yes plugin_pagespeed_detailed: yes
plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }} plugin_pagespeed_screenshot: yes
- name: ${{ matrix.template }} > Plugin > Music (playlist - apple) - name: ${{ matrix.template }} > Plugin > Music (playlist - apple)
uses: lowlighter/metrics@master uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -100,7 +89,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -112,7 +101,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -125,7 +114,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -137,18 +126,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_isocalendar: yes
- name: ${{ matrix.template }} > Plugin > Isocalendar (full year)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -160,25 +138,12 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_habits: yes plugin_habits: yes
plugin_habits_from: 5 plugin_habits_from: 5
- name: ${{ matrix.template }} > Plugin > Habits (charts)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_habits: yes
plugin_habits_from: 5
plugin_habits_facts: no
plugin_habits_charts: yes plugin_habits_charts: yes
- name: ${{ matrix.template }} > Plugin > Languages - name: ${{ matrix.template }} > Plugin > Languages
@@ -186,7 +151,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
@@ -197,32 +162,22 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_followup: yes plugin_followup: yes
- name: ${{ matrix.template }} > Plugin > Lines - name: ${{ matrix.template }} > Plugin > Lines and Traffic
uses: lowlighter/metrics@master uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_lines: yes plugin_lines: yes
- name: ${{ matrix.template }} > Plugin > Traffic
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_traffic: yes plugin_traffic: yes
- name: ${{ matrix.template }} > Plugin > Gists - name: ${{ matrix.template }} > Plugin > Gists
@@ -230,70 +185,49 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_gists: yes plugin_gists: yes
- name: ${{ matrix.template }} > Plugin > Topics (stars)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_topics: yes
plugin_topics_sort: stars
- name: ${{ matrix.template }} > Plugin > Topics (activity)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 1
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_topics: yes
plugin_topics_sort: activity
- name: ${{ matrix.template }} > Plugin > Topics (starred) - name: ${{ matrix.template }} > Plugin > Topics (starred)
uses: lowlighter/metrics@master uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_topics: yes plugin_topics: yes
plugin_topics_sort: starred plugin_topics_mode: starred
plugin_topics_sort: random
- name: ${{ matrix.template }} > Plugin > Topics (random) - name: ${{ matrix.template }} > Plugin > Topics (mastered)
uses: lowlighter/metrics@master uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_topics: yes plugin_topics: yes
plugin_topics_sort: random plugin_topics_mode: mastered
plugin_topics_sort: stars
- name: ${{ matrix.template }} > Plugin > Projects - name: ${{ matrix.template }} > Plugin > Projects
uses: lowlighter/metrics@master uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes
plugin_projects: yes plugin_projects: yes
plugin_projects_repositories: lowlighter/metrics/projects/1
plugin_projects_limit: 2 plugin_projects_limit: 2
- name: ${{ matrix.template }} > Plugin > Tweets - name: ${{ matrix.template }} > Plugin > Tweets
@@ -301,7 +235,7 @@ jobs:
with: with:
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes dryrun: yes
repositories: 1 repositories: 0
template: ${{ matrix.template }} template: ${{ matrix.template }}
base: "" base: ""
plugins_errors_fatal: yes plugins_errors_fatal: yes

View File

@@ -6,69 +6,98 @@ Nice ! Read the few sections below to understand how this project is structured.
### 👨‍💻 General informations ### 👨‍💻 General informations
#### Adding new metrics and plugins through GraphQL API, REST API or Third-Party service #### Adding new metrics through GraphQL API, REST API or Third-Party service
When updating a single template, changes should be made through [GitHub GraphQL API](https://docs.github.com/en/graphql) by editing the GraphQL query from `templates/*/query.graphql`. Computed or formatting can be made in `templates/*/template.mjs`. When possible, try to use the [GitHub GraphQL API](https://docs.github.com/en/graphql) by editing queries in `source/queries` or the [GitHub Rest API](https://docs.github.com/en/rest). Use `puppeteer` in last resort.
Raw queried data should be exposed directly into `data.user`, whereas computed data should be stored in `data.computed`.
When making a new plugin, a new folder with its name must be created in `src/plugins`. Data computing and formatting should be made in `templates/*/template.mjs` if it's template specific, or in `templates/common.mjs` if it can be made available for all templates.
Plugins entry point `src/plugins/*/index.mjs` will have access to `{login, q, imports, data, computed, rest, graphql}`.
It should be self-sufficient and independant from others plugins and used template. Raw queried data should be be exposed directly into `data.user`, whereas computed data should be stored in `data.computed`.
Data generated should be exposed in `data.computed.plugins[plugin]` where `plugin` is the plugin's name.
#### Updating SVG templates #### Updating SVG templates
The SVG templates are located in `templates/*/image.svg` and include CSS from `templates/*/style.css`. SVG templates are located in `templates/*/image.svg` and include CSS from `templates/*/style.css`.
It is rendered with [EJS](https://github.com/mde/ejs) so it is actually possible to include variables (e.g. `<%= user.name %>`) and execute simple code, like control statements. These are rendered through [EJS](https://github.com/mde/ejs), so it is actually possible to include variables (e.g. `<%= user.name %>`) and execute simple code, like control statements.
Plugins errors should be handled gracefully by displaying the error message. Exposed variables contains `user`, `computed` and `plugins` data, along with custom `style` and `fonts`.
#### Metrics server and GitHub action #### Creating a new plugin
Unless when integrating new features directly tied to these, it is not needed to edit them when creating a new template or plugin. Start by creating a new folder in `source/plugins`, along with its entry point `index.mjs`.
Keep in mind that SVG image is actually generated from `src/metrics.mjs`, independently from the metrics server and GitHub action.
Metrics server code is located in `src/app.mjs` and instantiates an `express` server app, `octokit`s instances, middlewares (like rate-limiter) and routes. A plugin function has access to a lot of data, such as user's `login`, `rest` and `graphql` GitHub API handlers, `imports` of various utilitaries functions and modules, and various data. See others plugins for examples.
GitHub action code is located in `action/index.mjs` and instantiates `octokit`s instances and retrieves action parameters. Plugins should be self-sufficient and independant from others plugins.
It then use directly `src/metrics.mjs` to generate the SVG image and commit them to user's repository.
Plugins errors should be handled gracefully by displaying an error message when it fails.
For user's convenience, a placeholder image can be generated to preview metrics without executing queries.
This use a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which "fills" with dummy data, though it has its limit (especially with iterable structures) so you may need to patch the placeholder function. It is located in `source/metrics.mjs`
When adding new options, be sure to patch both `action.yml` and `action/index.mjs` to support them.
#### Metrics web instance
Web instance code is located in `source/app.mjs` and instantiates an `express` server app.
#### GitHub action
GitHub action code is located in `action/index.mjs` and retrieves action parameters, which are converted into `q` parameters.
Once SVG image is generated through `source/metrics.mjs`, it is committed to user's repository.
#### Testing new features #### Testing new features
To test new features, setup a metrics server with a test token and `debug` mode enabled. To test new features, setup a metrics web instance with a personal token and `debug` mode enabled.
It allows fast prototyping and to rapidly test SVG renders in a browser. You can then test SVG renders in your browser and ensure that everything works as expected.
### 🗛 Fonts
Follow the following process to integrate custom fonts.
It should be avoided when possible as it increases drastically the size of generated metrics.
1. Find a font on [fonts.google.com](https://fonts.google.com/)
- Select regular, bold, italic and bold+italic fonts
- Open `embed` tab and extract the `href`
2. Open extracted `href` and append `&text=` params with used characters from SVG
- e.g. `&text=%26%27"%7C%60%5E%40°%3F!%23%24%25()*%2B%2C-.%2F0123456789%3A%3B<%3D>ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5D_abcdefghijklmnopqrstuvwxyz%7B%7D~─└├▇□✕`
3. Download each font file from url links from the generated stylesheet
4. Convert them into base64 with `woff` extension on [transfonter.org]https://transfonter.org/) and download archive
5. Extract archive and copy the content of the generated stylesheet to `templates/*/fonts.css`
6. Update your template
- Include `<defs><style><%= fonts %></style></defs>` to your `templates/*/image.svg`
- Edit your `templates/*/style.css` to use yout new font
### 🗂️ Project structure ### 🗂️ Project structure
#### Metrics generator #### Metrics generator
* `src/setup.mjs` contains the configuration setup * `source/setup.mjs` contains configuration setup
* `src/metrics.mjs` contains the metrics renderer * `source/metrics.mjs` contains the metrics renderer
* `src/templates/*` contains templates files * `source/templates/*` contains templates files
* `src/templates/*/image.svg` contains the template used by the generated SVG image * `source/templates/*/image.svg` contains the image template used to render metrics
* `src/templates/*/query.graphql` is the GraphQL query sent to GitHub GraphQL API * `source/templates/*/style.css` contains the style used to render metrics
* `src/templates/*/style.css` contains the style used by the generated SVG image * `source/templates/*/fonts.css` contains additional fonts used to render metrics
* `src/templates/*/template.mjs` contains the code which prepares data for rendering * `source/templates/*/template.mjs` contains the code used to prepare metrics data before rendering
* `src/plugins/*` contains the source code of metrics plugins * `source/plugins/*` contains source code of plugins
* `source/queries/*` contains GraphQL queries
#### Metrics server instance #### Web instance
* `index.mjs` contains the metrics server entry point * `index.mjs` contains metrics web instance entry point
* `src/app.mjs` contains the metrics server code which serves, renders, restricts/rate limit, etc. * `source/app.mjs` contains metrics web instance source code
* `src/html/*` contains the metrics server static files * `source/html/*` contains metrics web instance static files
#### GitHub action #### GitHub action
* `action.yml` contains the GitHub action descriptor * `action.yml` contains GitHub action descriptor
* `action/index.mjs` contains the GitHub action code * `action/index.mjs` contains GitHub action source code
* `action/dist/index.js` contains compiled the GitHub action code (auto-generated) * `action/dist/index.js` contains compiled the GitHub action code (auto-generated)
#### Others #### Others
* `tests/metrics.mjs` contains tests * `tests/metrics.mjs` contains tests
* `utils/build.mjs` allows to rebuild plugins and template indexes and GitHub action * `utils/build.mjs` contains a tool used to rebuild plugins and template indexes and GitHub action
### 📦 Used packages ### 📦 Used packages
@@ -100,17 +129,3 @@ It allows fast prototyping and to rapidly test SVG renders in a browser.
* To print colors in console * To print colors in console
* [babel/minify](https://github.com/babel/minify) * [babel/minify](https://github.com/babel/minify)
* To minify code * To minify code
### 🗛 Fonts
1. Find a font on [fonts.google.com](https://fonts.google.com/)
- Select regular, bold, italic and bold+italic fonts
- Open `embed` tab and extract the `href`
2. Open extracted `href` and append `&text=` params with used characters from SVG
- e.g. `&text=%26%27"%7C%60%5E%40°%3F!%23%24%25()*%2B%2C-.%2F0123456789%3A%3B<%3D>ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5D_abcdefghijklmnopqrstuvwxyz%7B%7D~─└├▇□✕`
3. Download each font file from url links from the generated stylesheet
4. Convert them into base64 with `woff` extension on [transfonter.org]https://transfonter.org/) and download archive
5. Extract archive and copy the content of the generated stylesheet to `templates/*/fonts.css`
6. Update your template
- Include `<defs><style><%= fonts %></style></defs>` to your `templates/*/image.svg`
- Edit your `templates/*/style.css` to use yout new font

112
README.md
View File

@@ -18,21 +18,26 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste
<a href="https://github.com/lowlighter/metrics#%EF%B8%8F-pagespeed"> <a href="https://github.com/lowlighter/metrics#%EF%B8%8F-pagespeed">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.svg" alt="" width="400">
</a> </a>
<details><summary>Alternate version</summary> <details><summary>Detailed audit version</summary>
<a href="https://github.com/lowlighter/metrics#%EF%B8%8F-pagespeed"> <a href="https://github.com/lowlighter/metrics#%EF%B8%8F-pagespeed">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.detailed.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.detailed.svg" alt="" width="400">
</a> </a>
</detail> </details>
<details><summary>With screenshot version</summary>
<a href="https://github.com/lowlighter/metrics#%EF%B8%8F-pagespeed">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.screenshot.svg" alt="" width="400">
</a>
</details>
</td> </td>
<td> <td>
<a href="https://github.com/lowlighter/metrics#-isometric-calendar"> <a href="https://github.com/lowlighter/metrics#-isometric-calendar">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.svg" alt="" width="400">
</a> </a>
<details><summary>Alternate version</summary> <details><summary>Full year version</summary>
<a href="https://github.com/lowlighter/metrics#-isometric-calendar"> <a href="https://github.com/lowlighter/metrics#-isometric-calendar">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.fullyear.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.fullyear.svg" alt="" width="400">
</a> </a>
</detail> </details>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -68,17 +73,22 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste
</td> </td>
</tr> </tr>
<tr> <tr>
<th><a href="https://github.com/lowlighter/metrics#-lines">📌 Starred topics plugin</a></th> <th><a href="https://github.com/lowlighter/metrics#-topics">📌 Starred topics plugin</a></th>
<th><a href="https://github.com/lowlighter/metrics#-traffic">🗂️ Active projects plugin</a></th> <th><a href="https://github.com/lowlighter/metrics#%EF%B8%8F-projects">🗂️ Active projects plugin</a></th>
</tr> </tr>
<tr> <tr>
<td> <td>
<a href="https://github.com/lowlighter/metrics#-lines"> <a href="https://github.com/lowlighter/metrics#-topics">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.svg" alt="" width="400">
</a> </a>
<details open><summary>Mastered and known technologies version</summary>
<a href="https://github.com/lowlighter/metrics#-topics">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.mastered.svg" alt="" width="400">
</a>
</details>
</td> </td>
<td> <td>
<a href="https://github.com/lowlighter/metrics#-traffic"> <a href="https://github.com/lowlighter/metrics#%EF%B8%8F-projects">
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.projects.svg" alt="" width="400"> <img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.projects.svg" alt="" width="400">
</a> </a>
</td> </td>
@@ -489,23 +499,23 @@ Used template defaults to the `classic` one.
<tr> <tr>
<th>Classic</th> <th>Classic</th>
<td>✔️</td> <td>✔️</td>
<td><span title="'plugin_pagespeed_screenshot' is available on @master">✔️<sup>N</sup></span></td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td><span title="'plugin_topics_mode' is available on @master">✔️<sup>N</sup></span></td>
<td><span title="'plugin_projects_repositories' is available on @master">✔️<sup>N</sup></span></td>
<td>✔️</td> <td>✔️</td>
<td>✔️</td> <td>✔️</td>
<td>✔️</td> <td>✔️</td>
<td>✔️</td> <td>✔️</td>
<td>✔️</td> <td>✔️</td>
<td>✔️</td> <td><span title="'Files' and 'Comments' are available on @master">✔️<sup>N</sup></span></td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
</tr> </tr>
<tr> <tr>
<th>Terminal</th> <th>Terminal</th>
<td><span title="'Available for hire' and 'Cake day' are currently not displayed">✔️<sup>P</sup></span></td> <td><span title="'Available for hire' and 'Cake day' are not displayed">✔️<sup>P</sup></span></td>
<td>✔️</td> <td>✔️</td>
<td>❌</td> <td>❌</td>
<td>❌</td> <td>❌</td>
@@ -520,12 +530,30 @@ Used template defaults to the `classic` one.
<td>❌</td> <td>❌</td>
<td>✔️</td> <td>✔️</td>
</tr> </tr>
<tr>
<th>Repository<sup>MR</sup></th>
<td>✔️</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>✔️</td>
<td>❌</td>
<td>✔️</td>
<td>✔️</td>
<td>✔️</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
</tr>
</table> </table>
**Legend** **Legend**
* **P** : Partial support *(Hover cell for more informations)* * **P** : Partial support *(Hover cell for more informations)*
* **M** : Plugin is not released yet but is available on `@master` * **M** : Feature is not released yet but is available on `@master`
* **N** : Plugin is already released, but new features are available on `@master` * **N** : Feature is already released, but new ones are available on `@master`
* **R** : Repository template (all plugins content will be restricted to related repository)
## 🧩 Plugins ## 🧩 Plugins
@@ -600,6 +628,20 @@ Add the following to your workflow instead :
plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }} plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
``` ```
🚧 The feature below is only available on @master
You can also display the screenshot taken by PageSpeed API :
![Pagespeed plugin (screenshot)](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.screenshot.svg)
Add the following to your workflow :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_pagespeed_screenshot: yes
```
</details> </details>
### 📅 Isometric calendar ### 📅 Isometric calendar
@@ -872,6 +914,21 @@ Add the following to your workflow :
plugin_topics_limit: 15 plugin_topics_limit: 15
``` ```
🚧 The feature below is only available on @master
It is possible to display starred topics as `Mastered and known technologies` instead :
![Topics plugin (mastered)](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.mastered.svg)
Add the following to your workflow instead :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_topics: yes
plugin_topics_mode: mastered
```
</details> </details>
### 🗂️ Projects ### 🗂️ Projects
@@ -914,6 +971,23 @@ Fill the informations and set visibility to *public* :
</details> </details>
🚧 The feature below is only available on @master
It is possible to display projects related to repositories along with personal projects.
To do so, open your repository project and retrieve the last url endpoint, in the format `:user/:repository/projects/:project_id` (for example, `lowlighter/metrics/projects/1`) and add it in the `plugin_projects_repositories` option. Enable `Track project progress` in the project settings to display a progress bar in generated metrics.
![Add a repository project](.github/readme/imgs/plugin_projects_repositories.png)
Add the following to your workflow :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_projects: yes
plugin_projects_repositories: :user/:repository1/projects/:project_id, :user/:repository2/projects/:project_id, ...
```
</details> </details>
### 👨‍💻 Lines ### 👨‍💻 Lines

View File

@@ -59,6 +59,13 @@ inputs:
description: Template to use description: Template to use
default: classic default: classic
# Raw query parameters (JSON string)
# Some templates may require additional parameters which you can specify here
# Do not use this option to pass plugins parameters as they'll be overwritten by the other options
query:
description: Additional query parameters
default: "{}"
# Template base content # Template base content
# Pass a string of comma-separated values # Pass a string of comma-separated values
# To disable everything (like if you want to use a plugin as standalone), pass an empty string # To disable everything (like if you want to use a plugin as standalone), pass an empty string
@@ -86,6 +93,11 @@ inputs:
description: Display additional PageSpeed metrics description: Display additional PageSpeed metrics
default: no default: no
# Display the final screenshot of audited website taken by PageSpeed audit
plugin_pagespeed_screenshot:
description: Display a screenshot of your website
default: no
# PageSpeed API token (optional, avoid hitting requests limit) # PageSpeed API token (optional, avoid hitting requests limit)
# See https://developers.google.com/speed/docs/insights/v5/get-started for more informations # See https://developers.google.com/speed/docs/insights/v5/get-started for more informations
plugin_pagespeed_token: plugin_pagespeed_token:
@@ -243,6 +255,15 @@ inputs:
description: Display starred topics description: Display starred topics
default: no default: no
# Topics plugin mode
# Change the way topics are displayed
# Supported values are :
# - "starred" to display starred topics as interests labels
# - "mastered" to display starred topics as mastered/known technologies icons
plugin_topics_mode:
description: Display starred topics
default: "starred"
# Sorting method of displayed topics # Sorting method of displayed topics
# Supported values are : # Supported values are :
# - "stars" to sort them from most starred to least starred # - "stars" to sort them from most starred to least starred
@@ -253,11 +274,11 @@ inputs:
description: Sorting method of starred topics description: Sorting method of starred topics
default: "stars" default: "stars"
# Number of topics to display (between 1 and 20) # Number of topics to display (between 0 and 20)
# If more topics must be displayed, they will be grouped in an ellipsis # If more topics must be displayed, they will be grouped in an ellipsis
plugin_topics_limit: plugin_topics_limit:
description: Number of starred topics to display description: Number of starred topics to display
default: 15 default: 0
# Projects plugin # Projects plugin
# Display active projects # Display active projects
@@ -265,6 +286,14 @@ inputs:
description: Display active projects description: Display active projects
default: no default: no
# Display active repository projects
# By default, only user owned projects are displayed, with this option it is possible to display projects from repositories
# List of comma-separated projects identifier, in the following format :user/:repo/projects/:project_id
# The limit of displayed projects will automatically be updated so listed projects will fit
plugin_projects_repositories:
description: List of repository project identifiers to disaplay
default: ""
# Number of active projects to display # Number of active projects to display
# Between 1 and 100 # Between 1 and 100
plugin_projects_limit: plugin_projects_limit:

70
action/dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -110,8 +110,10 @@
if (plugins.pagespeed.enabled) { if (plugins.pagespeed.enabled) {
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || "" plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || ""
q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`)) q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`))
q[`pagespeed.screenshot`] = bool(core.getInput(`plugin_pagespeed_screenshot`))
console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`) console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`)
console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`) console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`)
console.log(`Pagespeed screenshot | ${q["pagespeed.screenshot"]}`)
} }
//Languages //Languages
if (plugins.languages.enabled) { if (plugins.languages.enabled) {
@@ -156,16 +158,18 @@
} }
//Topics //Topics
if (plugins.topics.enabled) { if (plugins.topics.enabled) {
for (const option of ["sort", "limit"]) for (const option of ["mode", "sort", "limit"])
q[`topics.${option}`] = core.getInput(`plugin_topics_${option}`) || null q[`topics.${option}`] = core.getInput(`plugin_topics_${option}`) || null
console.log(`Topics mode | ${q["topics.mode"] || "(default)"}`)
console.log(`Topics sort mode | ${q["topics.sort"] || "(default)"}`) console.log(`Topics sort mode | ${q["topics.sort"] || "(default)"}`)
console.log(`Topics limit | ${q["topics.limit"] || "(default)"}`) console.log(`Topics limit | ${q["topics.limit"] || "(default)"}`)
} }
//Projects //Projects
if (plugins.projects.enabled) { if (plugins.projects.enabled) {
for (const option of ["limit"]) for (const option of ["limit", "repositories"])
q[`projects.${option}`] = core.getInput(`plugin_projects_${option}`) || null q[`projects.${option}`] = core.getInput(`plugin_projects_${option}`) || null
console.log(`Projects limit | ${q["projects.limit"] || "(default)"}`) console.log(`Projects limit | ${q["projects.limit"] || "(default)"}`)
console.log(`Projects repositories | ${q["projects.repositories"] || "(none)"}`)
} }
//Tweets //Tweets
if (plugins.tweets.enabled) { if (plugins.tweets.enabled) {
@@ -184,8 +188,10 @@
const die = bool(core.getInput("plugins_errors_fatal")) const die = bool(core.getInput("plugins_errors_fatal"))
console.log(`Plugin errors | ${die ? "die" : "warn"}`) console.log(`Plugin errors | ${die ? "die" : "warn"}`)
//Built query //Build query
q = {...q, base:false, ...base, ...config, repositories, template} const query = JSON.parse(core.getInput("query") || "{}")
console.log(`Query additional params | ${JSON.stringify(query)}`)
q = {...query, ...q, base:false, ...base, ...config, repositories, template}
//Render metrics //Render metrics
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die}) const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die})

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "metrics", "name": "metrics",
"version": "2.9.0-beta", "version": "2.10.0-beta",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "metrics", "name": "metrics",
"version": "2.9.0", "version": "2.10.0-beta",
"description": "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 !", "description": "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 !",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {

View File

@@ -27,7 +27,7 @@
}, },
"habits":{ "//":"Habits plugin", "habits":{ "//":"Habits plugin",
"enabled":false, "//":"Enable or disable coding habits metrics", "enabled":false, "//":"Enable or disable coding habits metrics",
"from":100, "//":"Number of activity events to base habits on (up to 100)" "from":200, "//":"Number of activity events to base habits on (up to 1000)"
}, },
"languages":{ "//":"Languages plugin", "languages":{ "//":"Languages plugin",
"enabled":true, "//":"Enable or disable most used languages metrics" "enabled":true, "//":"Enable or disable most used languages metrics"

View File

@@ -55,7 +55,8 @@
"languages.ignored":"", "languages.ignored":"",
"languages.skipped":"", "languages.skipped":"",
"pagespeed.detailed":false, "pagespeed.detailed":false,
"habits.from":100, "pagespeed.screenshot":false,
"habits.from":200,
"habits.days":14, "habits.days":14,
"habits.facts":true, "habits.facts":true,
"habits.charts":false, "habits.charts":false,
@@ -65,6 +66,8 @@
"posts.source":"dev.to", "posts.source":"dev.to",
"isocalendar.duration":"half-year", "isocalendar.duration":"half-year",
"projects.limit":4, "projects.limit":4,
"projects.repositories":"",
"topics.mode":"starred",
"topics.sort":"stars", "topics.sort":"stars",
"topics.limit":12, "topics.limit":12,
"tweets.limit":2, "tweets.limit":2,
@@ -78,6 +81,7 @@
descriptions:{ descriptions:{
classic:"Classic template", classic:"Classic template",
terminal:"Terminal template", terminal:"Terminal template",
repository:"(hidden)",
}, },
}, },
generated:{ generated:{

View File

@@ -28,7 +28,7 @@
<div class="step"> <div class="step">
<h2>2. Select a template</h2> <h2>2. Select a template</h2>
<div class="templates"> <div class="templates">
<label v-for="template in templates.list" :key="template"> <label v-for="template in templates.list" :key="template" v-show="templates.descriptions[template] !== '(hidden)'">
<input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending"> <input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending">
{{ templates.descriptions[template] || template }} {{ templates.descriptions[template] || template }}
</label> </label>
@@ -78,6 +78,10 @@
Detailed PageSpeed report Detailed PageSpeed report
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load"> <input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load">
</label> </label>
<label>
Include a website screenshot
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load">
</label>
</div> </div>
<div class="options-group" v-if="plugins.enabled.languages"> <div class="options-group" v-if="plugins.enabled.languages">
<h4>{{ plugins.descriptions.languages }}</h4> <h4>{{ plugins.descriptions.languages }}</h4>
@@ -134,6 +138,13 @@
</div> </div>
<div class="options-group" v-if="plugins.enabled.topics"> <div class="options-group" v-if="plugins.enabled.topics">
<h4>{{ plugins.descriptions.topics }}</h4> <h4>{{ plugins.descriptions.topics }}</h4>
<label>
Topics display mode
<select v-model="plugins.options['topics.mode']" @change="load">
<option value="starred">Starred topics</option>
<option value="mastered">Known and mastered technologies</option>
</select>
</label>
<label> <label>
Topics sorting Topics sorting
<select v-model="plugins.options['topics.sort']"> <select v-model="plugins.options['topics.sort']">
@@ -154,6 +165,10 @@
Number of projects to display Number of projects to display
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load"> <input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load">
</label> </label>
<label>
Repositories projects to display (comma separated)
<input type="text" v-model="plugins.options['projects.repositories']" @change="load">
</label>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -27,8 +27,9 @@
const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template)))) if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
throw new Error("unsupported template") throw new Error("unsupported template")
const {query, image, style, fonts} = conf.templates[template] const {image, style, fonts} = conf.templates[template]
const data = {base:{}, config:{}} const queries = conf.queries
const data = {base:{}, config:{}, errors:[], plugins:{}, computed:{}}
//Base parts //Base parts
{ {
@@ -44,28 +45,38 @@
else { else {
//Query data from GitHub API //Query data from GitHub API
console.debug(`metrics/compute/${login} > graphql query`) console.debug(`metrics/compute/${login} > graphql query`)
Object.assign(data, await graphql(query Object.assign(data, await graphql(queries.common({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString()})))
.replace(/[$]login/, `"${login}"`) //Query repositories from GitHub API
.replace(/[$]repositories/, `${repositories}`) {
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`) //Iterate through repositories
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`) let cursor = null
)) let pushed = 0
do {
console.debug(`metrics/compute/${login} > retrieving repositories after ${cursor}`)
const {user:{repositories:{edges, nodes}}} = await graphql(queries.repositories({login, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, 100)}))
cursor = edges?.[edges?.length-1]?.cursor
data.user.repositories.nodes.push(...nodes)
pushed = nodes.length
} while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories))
//Limit repositories
console.debug(`metrics/compute/${login} > keeping only ${repositories} repositories`)
data.user.repositories.nodes.splice(repositories)
console.debug(`metrics/compute/${login} > loaded ${data.user.repositories.nodes.length} repositories`)
}
//Compute metrics //Compute metrics
console.debug(`metrics/compute/${login} > compute`) console.debug(`metrics/compute/${login} > compute`)
const computer = Templates[template].default || Templates[template] const computer = Templates[template].default || Templates[template]
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}}) await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
const promised = await Promise.all(pending) const promised = await Promise.all(pending)
//Check plugins errors //Check plugins errors
{ {
const errors = promised.filter(({result = null}) => result?.error) const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
if (die) { if (errors.length) {
if (errors.length) console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
throw new Error(`${errors.length} error${s(errors.length)} found...`) if (die)
} throw new Error(`An error occured during rendering, dying`)
else { else
console.warn(`${errors.length} error${s(errors.length)} found, ignoring...`)
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256})) console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
} }
} }
@@ -159,7 +170,7 @@
/** Placeholder generator */ /** Placeholder generator */
function placeholder({data, conf, q}) { function placeholder({data, conf, q}) {
//Proxifier //Proxifier
const proxify = (target) => typeof target === "object" ? new Proxy(target, { const proxify = (target) => (typeof target === "object")&&(target) ? new Proxy(target, {
get(target, property) { get(target, property) {
//Primitive conversion //Primitive conversion
if (property === Symbol.toPrimitive) if (property === Symbol.toPrimitive)
@@ -196,11 +207,11 @@
[key, proxify({ [key, proxify({
posts:{source:"########", list:new Array("posts.limit" in q ? Math.max(Number(q["posts.limit"])||0, 0) : 2).fill({title:"###### ###### ####### ######", date:"####"})}, posts:{source:"########", list:new Array("posts.limit" in q ? Math.max(Number(q["posts.limit"])||0, 0) : 2).fill({title:"###### ###### ####### ######", date:"####"})},
music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})}, music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})},
pagespeed:{detailed:!!q["pagespeed.detailed"], scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))}, pagespeed:{detailed:!!q["pagespeed.detailed"], screenshot:!!q["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null, scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
followup:{issues:{count:0}, pr:{count:0}}, followup:{issues:{count:0}, pr:{count:0}},
habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}}, habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}},
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))}, languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
topics:{list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]}, topics:{mode:"topics.mode" in q ? q["topics.mode"] : "starred", list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]}, projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
tweets:{profile:{username:"########", verified:false}, list:[...new Array("tweets.limit" in q ? Math.max(Number(q["tweets.limit"])||0, 0) : 2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]}, tweets:{profile:{username:"########", verified:false}, list:[...new Array("tweets.limit" in q ? Math.max(Number(q["tweets.limit"])||0, 0) : 2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]},
}[key]??{})] }[key]??{})]

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) { export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution //Plugin execution
try { try {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
@@ -7,40 +7,22 @@
return null return null
//Retrieve gists from graphql api //Retrieve gists from graphql api
console.debug(`metrics/compute/${login}/plugins > gists > querying api`) console.debug(`metrics/compute/${login}/plugins > gists > querying api`)
const {user:{gists}} = await graphql(` const {user:{gists}} = await graphql(queries.gists({login}))
query Gists {
user(login: "${login}") {
gists(last: 100) {
totalCount
nodes {
stargazerCount
isFork
forks {
totalCount
}
comments {
totalCount
}
}
}
}
}
`
)
//Iterate through gists //Iterate through gists
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`) console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`)
let stargazers = 0, forks = 0, comments = 0 let stargazers = 0, forks = 0, comments = 0, files = 0
for (const gist of gists.nodes) { for (const gist of gists.nodes) {
//Skip forks //Skip forks
if (gist.isFork) if (gist.isFork)
continue continue
//Compute stars, forks and comments //Compute stars, forks, comments and files count
stargazers += gist.stargazerCount stargazers += gist.stargazerCount
forks += gist.forks.totalCount forks += gist.forks.totalCount
comments += gist.comments.totalCount comments += gist.comments.totalCount
files += gist.files.length
} }
//Results //Results
return {totalCount:gists.totalCount, stargazers, forks, comments} return {totalCount:gists.totalCount, stargazers, forks, files, comments}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) { export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution //Plugin execution
try { try {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
@@ -24,24 +24,7 @@
const calendar = {} const calendar = {}
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) { for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`) console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(` const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.calendar({login, from:from.toISOString(), to:to.toISOString()}))
query Calendar {
user(login: "${login}") {
calendar:contributionsCollection(from: "${from.toISOString()}", to: "${to.toISOString()}") {
contributionCalendar {
weeks {
contributionDays {
contributionCount
color
date
}
}
}
}
}
}
`
)
calendar[name] = weeks calendar[name] = weeks
} }
//Apply padding //Apply padding

View File

@@ -171,7 +171,7 @@
//Limit tracklist //Limit tracklist
if (limit > 0) { if (limit > 0) {
console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`) console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`)
tracks = tracks.slice(0, limit) tracks.splice(limit)
} }
//Convert artworks to base64 //Convert artworks to base64
console.debug(`metrics/compute/${login}/plugins > music > loading artworks`) console.debug(`metrics/compute/${login}/plugins > music > loading artworks`)

View File

@@ -6,7 +6,7 @@
if ((!enabled)||(!q.pagespeed)||(!data.user.websiteUrl)) if ((!enabled)||(!q.pagespeed)||(!data.user.websiteUrl))
return null return null
//Parameters override //Parameters override
let {"pagespeed.detailed":detailed = false} = q let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false} = q
//Duration in days //Duration in days
detailed = !!detailed detailed = !!detailed
//Format url if needed //Format url if needed
@@ -18,12 +18,18 @@
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`) console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
const scores = new Map() const scores = new Map()
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => { await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
//Perform audit
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`) console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`) const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)
console.debug(request.data) console.debug(request.data)
const {score, title} = request.data.lighthouseResult.categories[category] const {score, title} = request.data.lighthouseResult.categories[category]
scores.set(category, {score, title}) scores.set(category, {score, title})
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`) console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
//Store screenshot
if ((screenshot)&&(category === "performance")) {
result.screenshot = request.data.lighthouseResult.audits["final-screenshot"].details.data
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
}
})) }))
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")] result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
//Detailed metrics //Detailed metrics

View File

@@ -29,7 +29,7 @@
//Limit tracklist //Limit tracklist
if (limit > 0) { if (limit > 0) {
console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`) console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`)
posts = posts.slice(0, limit) posts.splice(limit)
} }
//Results //Results
return {source, list:posts} return {source, list:posts}

View File

@@ -1,38 +1,34 @@
//Setup //Setup
export default async function ({login, graphql, q}, {enabled = false} = {}) { export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
//Plugin execution //Plugin execution
try { try {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.projects)) if ((!enabled)||(!q.projects))
return null return null
//Parameters override //Parameters override
let {"projects.limit":limit = 4} = q let {"projects.limit":limit = 4, "projects.repositories":repositories = ""} = q
//Repositories projects
repositories = repositories?.split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? []
//Limit //Limit
limit = Math.max(1, Math.min(100, Number(limit))) limit = Math.max(repositories.length, Math.min(100, Number(limit)))
//Retrieve contribution calendar from graphql api //Retrieve user owned projects from graphql api
console.debug(`metrics/compute/${login}/plugins > projects > querying api`) console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
const {user:{projects}} = await graphql(` const {user:{projects}} = await graphql(queries.projects({login, limit}))
query Projects { //Retrieve repositories projects from graphql api
user(login: "${login}") { for (const identifier of repositories) {
projects(last: ${limit}, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) { //Querying repository project
totalCount console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
nodes { const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups
name const {user:{repository:{project}}} = await graphql(queries["projects.repository"]({user, repository, id}))
updatedAt //Adding it to projects list
progress { console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
doneCount project.name = `${project.name} (${user}/${repository})`
inProgressCount projects.nodes.unshift(project)
todoCount projects.totalCount++
enabled
} }
}
}
}
}
`
)
//Iterate through projects and format them //Iterate through projects and format them
console.debug(`metrics/compute/${login}/plugins > posts > processing ${projects.nodes.length} projects`) console.debug(`metrics/compute/${login}/plugins > projects > processing ${projects.nodes.length} projects`)
const list = [] const list = []
for (const project of projects.nodes) { for (const project of projects.nodes) {
//Format date //Format date
@@ -49,6 +45,9 @@
//Append //Append
list.push({name:project.name, updated, progress:{enabled, todo, doing, done, total:todo+doing+done}}) list.push({name:project.name, updated, progress:{enabled, todo, doing, done, total:todo+doing+done}})
} }
//Limit
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
list.splice(limit)
//Results //Results
return {list, totalCount:projects.totalCount} return {list, totalCount:projects.totalCount}
} }

View File

@@ -6,13 +6,15 @@
if ((!enabled)||(!q.topics)) if ((!enabled)||(!q.topics))
return null return null
//Parameters override //Parameters override
let {"topics.sort":sort = "stars", "topics.limit":limit = 15} = q let {"topics.sort":sort = "stars", "topics.mode":mode = "starred", "topics.limit":limit = (mode === "mastered" ? 0 : 15)} = q
//Shuffle //Shuffle
const shuffle = (sort === "random") const shuffle = (sort === "random")
//Sort method //Sort method
sort = {starred:"created", activity:"updated", stars:"stars", random:"created"}[sort] ?? "starred" sort = {starred:"created", activity:"updated", stars:"stars", random:"created"}[sort] ?? "starred"
//Limit //Limit
limit = Math.max(1, Math.min(20, Number(limit))) limit = Math.max(0, Math.min(20, Number(limit)))
//Mode
mode = ["starred", "mastered"].includes(mode) ? mode : "starred"
//Start puppeteer and navigate to topics //Start puppeteer and navigate to topics
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`) console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
let topics = [] let topics = []
@@ -49,11 +51,10 @@
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`) console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
topics = imports.shuffle(topics) topics = imports.shuffle(topics)
} }
//Limit topics //Limit topics (starred mode)
if (limit > 0) { if ((mode === "starred")&&(limit > 0)) {
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`) console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
const removed = topics.slice(limit) const removed = topics.splice(limit)
topics = topics.slice(0, limit)
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null}) topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
} }
//Convert icons to base64 //Convert icons to base64
@@ -66,8 +67,18 @@
//Escape HTML description //Escape HTML description
topic.description = imports.htmlescape(topic.description) topic.description = imports.htmlescape(topic.description)
} }
//Filter topics with icon (mastered mode)
if (mode === "mastered") {
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
topics = topics.filter(({icon}) => icon)
}
//Limit topics (mastered mode)
if ((mode === "mastered")&&(limit > 0)) {
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
topics.splice(limit)
}
//Results //Results
return {list:topics} return {mode, list:topics}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {

View File

@@ -0,0 +1,15 @@
query Calendar {
user(login: "$login") {
calendar:contributionsCollection(from: "$from", to: "$to") {
contributionCalendar {
weeks {
contributionDays {
contributionCount
color
date
}
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
query Metrics { query Metrics {
user(login: $login) { user(login: "$login") {
databaseId databaseId
name name
login login
@@ -11,45 +11,11 @@ query Metrics {
gists { gists {
totalCount totalCount
} }
repositories(last: $repositories, isFork: false, ownerAffiliations: OWNER) { repositories(last: 0, isFork: false, ownerAffiliations: OWNER) {
totalCount totalCount
totalDiskUsage totalDiskUsage
nodes { nodes {
name name
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 4) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
} }
} }
packages { packages {
@@ -75,7 +41,7 @@ query Metrics {
totalPullRequestContributions totalPullRequestContributions
totalPullRequestReviewContributions totalPullRequestReviewContributions
} }
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) { calendar:contributionsCollection(from: "$calendar.from", to: "$calendar.to") {
contributionCalendar { contributionCalendar {
weeks { weeks {
contributionDays { contributionDays {

20
src/queries/gists.graphql Normal file
View File

@@ -0,0 +1,20 @@
query Gists {
user(login: "$login") {
gists(last: 100) {
totalCount
nodes {
stargazerCount
isFork
forks {
totalCount
}
files {
name
}
comments {
totalCount
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
query Projects {
user(login: "$login") {
projects(last: $limit, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
name
updatedAt
progress {
doneCount
inProgressCount
todoCount
enabled
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
query Projects {
user(login: "$user") {
repository(name: "$repository") {
project(number: $id) {
name
updatedAt
progress {
doneCount
inProgressCount
todoCount
enabled
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
query Metrics {
user(login: "$login") {
repositories($after first: $repositories, isFork: false, ownerAffiliations: OWNER, orderBy: {field: UPDATED_AT, direction: DESC}) {
edges {
cursor
}
nodes {
name
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 8) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
query Metrics {
user(login: "$login") {
repository(name: "$repo") {
name
createdAt
diskUsage
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 8) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
}

View File

@@ -10,8 +10,10 @@
const logger = log ? console.debug : () => null const logger = log ? console.debug : () => null
logger(`metrics/setup > setup`) logger(`metrics/setup > setup`)
const templates = "src/templates" const templates = "src/templates"
const queries = "src/queries"
const conf = { const conf = {
templates:{}, templates:{},
queries:{},
settings:{}, settings:{},
statics:path.resolve("src/html"), statics:path.resolve("src/html"),
node_modules:path.resolve("node_modules"), node_modules:path.resolve("node_modules"),
@@ -52,22 +54,21 @@
continue continue
logger(`metrics/setup > load template [${name}]`) logger(`metrics/setup > load template [${name}]`)
const files = [ const files = [
`${templates}/${name}/query.graphql`,
`${templates}/${name}/image.svg`, `${templates}/${name}/image.svg`,
`${templates}/${name}/style.css`, `${templates}/${name}/style.css`,
`${templates}/${name}/fonts.css`, `${templates}/${name}/fonts.css`,
] ].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${templates}/${name}/`, `${templates}/classic/`)).map(file => path.resolve(file))
const [query, image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`)) const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
conf.templates[name] = {query, image, style, fonts} conf.templates[name] = {image, style, fonts}
logger(`metrics/setup > load template [${name}] > success`) logger(`metrics/setup > load template [${name}] > success`)
//Debug //Debug
if (conf.settings.debug) { if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, { Object.defineProperty(conf.templates, name, {
get() { get() {
logger(`metrics/setup > reload template [${name}]`) logger(`metrics/setup > reload template [${name}]`)
const [query, image, style, fonts] = files.map(file => `${fs.readFileSync(path.resolve(file))}`) const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
logger(`metrics/setup > reload template [${name}] > success`) logger(`metrics/setup > reload template [${name}] > success`)
return {query, image, style, fonts} return {image, style, fonts}
} }
}) })
} }
@@ -78,6 +79,39 @@
conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8")) conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8"))
} }
//Load queries
if (fs.existsSync(path.resolve(queries))) {
for (const query of await fs.promises.readdir(queries)) {
//Cache queries
const name = query.replace(/[.]graphql$/, "")
logger(`metrics/setup > load query [${name}]`)
conf.queries[`_${name}`] = `${await fs.promises.readFile(path.resolve(`${queries}/${query}`))}`
logger(`metrics/setup > load query [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.queries, `_${name}`, {
get() {
logger(`metrics/setup > reload query [${name}]`)
const raw = `${fs.readFileSync(path.resolve(`${queries}/${query}`))}`
logger(`metrics/setup > reload query [${name}] > success`)
return raw
}
})
}
}
}
else {
logger(`metrics/setup > load queries from build`)
conf.queries = JSON.parse(Buffer.from(`<#queries>`, "base64").toString("utf8"))
}
//Create queries formatters
Object.keys(conf.queries).map(name => conf.queries[name.substring(1)] = (vars = {}) => {
let query = conf.queries[name]
for (const [key, value] of Object.entries(vars))
query = query.replace(new RegExp(`[$]${key}`, "g"), value)
return query
})
//Conf //Conf
logger(`metrics/setup > setup > success`) logger(`metrics/setup > setup > success`)
return conf return conf

View File

@@ -5,7 +5,7 @@
+ (!!base.repositories)*108 + (!!base.repositories)*108
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16 + ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
+ (!!plugins.followup)*68 + (!!plugins.followup)*68
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20 + (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20 + (!!plugins.pagespeed?.screenshot)*330
+ (!!plugins.habits)*28 + (!!plugins.habits?.facts)*58 + (!!plugins.habits?.charts)*226 + (!!plugins.habits)*28 + (!!plugins.habits?.facts)*58 + (!!plugins.habits?.charts)*226
+ (!!plugins.languages)*96 + (!!plugins.languages)*96
+ (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0) + (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0)
@@ -380,11 +380,19 @@
<% } else { %> <% } else { %>
<section> <section>
<div class="field"> <div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg> <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>
<%= plugins.gists.stargazers %> Stargazer<%= s(plugins.gists.stargazers) %> <%= plugins.gists.files %> File<%= s(plugins.gists.files) %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg>
<%= plugins.gists.comments %> Comment<%= s(plugins.gists.comments) %>
</div> </div>
</section> </section>
<section> <section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
<%= plugins.gists.stargazers %> Stargazer<%= s(plugins.gists.stargazers) %>
</div>
<div class="field"> <div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<%= plugins.gists.forks %> Fork<%= s(plugins.gists.forks) %> <%= plugins.gists.forks %> Fork<%= s(plugins.gists.forks) %>
@@ -497,6 +505,13 @@
</section> </section>
</div> </div>
<% } %> <% } %>
<% if (plugins.pagespeed.screenshot) { %>
<div class="row">
<section>
<img class="screenshot" src="<%= plugins.pagespeed.screenshot %>" width="452" height="315"/>
</section>
</div>
<% } %>
<% } %> <% } %>
<% } %> <% } %>
@@ -585,7 +600,7 @@
<section> <section>
<h2 class="field"> <h2 class="field">
<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> <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>
Starred topics <%= {starred:"Starred topics", mastered:"Mastered technologies and topics"}[plugins.topics.mode] %>
</h2> </h2>
<div class="row"> <div class="row">
<% if (plugins.topics.error) { %> <% if (plugins.topics.error) { %>
@@ -598,9 +613,17 @@
<% } else { %> <% } else { %>
<section> <section>
<div class="topics fill-width"> <div class="topics fill-width">
<% if (plugins.topics.mode === "starred") { %>
<% for (const {name, description} of plugins.topics.list) { %> <% for (const {name, description} of plugins.topics.list) { %>
<div class="label" title="<%= description %>"><%= name.toLocaleLowerCase() %></div> <div class="label" title="<%= description %>"><%= name.toLocaleLowerCase() %></div>
<% } %> <% } %>
<% } else if (plugins.topics.mode === "mastered") { %>
<% for (const {name, icon} of plugins.topics.list) { %>
<% if (icon) { %>
<img src="data:image/png;base64,<%= icon %>" width="24" height="24" alt="<%= name %>" title="<%= name %>"/>
<% } %>
<% } %>
<% } %>
</div> </div>
</section> </section>
<% } %> <% } %>

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -226,6 +226,13 @@
fill: #e53935; fill: #e53935;
} }
.screenshot {
width: 452px;
height: 315px;
margin: 8px 14px 4px;
border-radius: 5px;
}
/* Music plugin */ /* Music plugin */
.tracklist { .tracklist {
display: flex; display: flex;
@@ -284,6 +291,11 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.topics img {
border-radius: 5px;
margin: 4px;
}
/* Tweets */ /* Tweets */
.tweet { .tweet {
font-size: 13px; font-size: 13px;

View File

@@ -2,7 +2,7 @@
import common from "./../common.mjs" import common from "./../common.mjs"
/** Template processor */ /** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) { export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Common //Common
await common(...arguments) await common(...arguments)
} }

View File

@@ -1,10 +1,9 @@
/** Template common processor */ /** Template common processor */
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) { export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Init //Init
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}} const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
const avatar = imports.imgb64(data.user.avatarUrl) const avatar = imports.imgb64(data.user.avatarUrl)
data.plugins = {}
console.debug(`metrics/compute/${login} > formatting common metrics`) console.debug(`metrics/compute/${login} > formatting common metrics`)
//Timezone config //Timezone config
@@ -24,7 +23,7 @@
pending.push((async () => { pending.push((async () => {
try { try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`) console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql}, plugins[name]) data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries}, plugins[name])
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (${data.plugins[name] !== null ? "success" : "skipped"})`) console.debug(`metrics/compute/${login}/plugins > ${name} > completed (${data.plugins[name] !== null ? "success" : "skipped"})`)
} }
catch (error) { catch (error) {

View File

@@ -2,10 +2,12 @@
//Imports //Imports
import classic from "./classic/template.mjs" import classic from "./classic/template.mjs"
import repository from "./repository/template.mjs"
import terminal from "./terminal/template.mjs" import terminal from "./terminal/template.mjs"
//Exports //Exports
export default { export default {
classic, classic,
repository,
terminal, terminal,
} }

View File

@@ -0,0 +1,220 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 0
+ (!!base.header)*42
+ (!!plugins.traffic)*18
+ (!!plugins.followup)*68
+ (!!base.metadata)*28
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
%>">
<defs><style><%= fonts %></style></defs>
<style>
<%= style %>
</style>
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
<% if (errors.length) { %>
<section>
<div class="row">
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= errors.map(({error}) => error.message).join(", ") %>
</div>
</div>
</section>
<% } else { %>
<% if (base.header) { %>
<section>
<div class="row">
<section>
<div class="field <%= computed.cakeday ? 'cakeday' : '' %>">
<% if (computed.cakeday) { %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 1.5a1.25 1.25 0 100 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707zM2 2.75c0 .45.108.875.3 1.25h-.55A1.75 1.75 0 000 5.75v2c0 .698.409 1.3 1 1.582v4.918c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0015 14.25V9.332c.591-.281 1-.884 1-1.582v-2A1.75 1.75 0 0014.25 4h-.55a2.75 2.75 0 00-2.45-4c-.984 0-1.874.42-2.57 1.23A5.086 5.086 0 008 2.274a5.086 5.086 0 00-.68-1.042C6.623.42 5.733 0 4.75 0A2.75 2.75 0 002 2.75zM8.941 4h2.309a1.25 1.25 0 100-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793zm-1.84 1.5H1.75a.25.25 0 00-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-.149zm1.649 0V8h5.5a.25.25 0 00.25-.25v-2a.25.25 0 00-.25-.25h-5.5zm0 4h4.75v4.75a.25.25 0 01-.25.25h-4.5v-5zm-1.5 0v5h-4.5a.25.25 0 01-.25-.25V9.5h4.75z"></path></svg>
Created <%= computed.registration %>
<% } else { %>
<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 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Created <%= computed.registration %>
<% } %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" d="M2.5 3.5c0-.133.058-.318.282-.55.227-.237.592-.484 1.1-.708C4.899 1.795 6.354 1.5 8 1.5c1.647 0 3.102.295 4.117.742.51.224.874.47 1.101.707.224.233.282.418.282.551 0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 5.205 9.646 5.5 8 5.5c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707-.224-.233-.282-.418-.282-.551zM1 3.5c0-.626.292-1.165.7-1.59.406-.422.956-.767 1.579-1.041C4.525.32 6.195 0 8 0c1.805 0 3.475.32 4.722.869.622.274 1.172.62 1.578 1.04.408.426.7.965.7 1.591v9c0 .626-.292 1.165-.7 1.59-.406.422-.956.767-1.579 1.041C11.476 15.68 9.806 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.62-1.579-1.04-.408-.426-.7-.965-.7-1.591v-9zM2.5 8V5.724c.241.15.503.286.779.407C4.525 6.68 6.195 7 8 7c1.805 0 3.475-.32 4.722-.869.275-.121.537-.257.778-.407V8c0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 9.705 9.646 10 8 10c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707C2.558 8.318 2.5 8.133 2.5 8zm0 2.225V12.5c0 .133.058.318.282.55.227.237.592.484 1.1.708 1.016.447 2.471.742 4.118.742 1.647 0 3.102-.295 4.117-.742.51-.224.874-.47 1.101-.707.224-.233.282-.418.282-.551v-2.275c-.241.15-.503.285-.778.406-1.247.549-2.917.869-4.722.869-1.805 0-3.475-.32-4.721-.869a6.236 6.236 0 01-.779-.406z"></path></svg>
<%= computed.diskUsage %> used
</div>
<% if (plugins.traffic) { %>
<div class="field <%= plugins.traffic.error ? 'error' : '' %>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
<% if (plugins.traffic.error) { %>
<%= plugins.traffic.error.message %>
<% } else { %>
<%= plugins.traffic.views.count %> view<%= s(plugins.traffic.views.count) %> in last two weeks
<% } %>
</div>
<% } %>
</section>
<section>
<div class="field calendar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <%= computed.calendar.length*15 %> 11" width="<%= computed.calendar.length*15 %>" height="16">
<g>
<% for (const [x, {color}] of Object.entries(computed.calendar)) { %>
<rect class="day" x="<%= x*15 %>" y="0" width="11" height="11" fill="<%= color %>" rx="2" ry="2" />
<% } %>
</g>
</svg>
</div>
<% if (plugins.lines) { %>
<div class="field <%= plugins.lines.error ? 'error' : '' %>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V4.664a.25.25 0 00-.073-.177l-2.914-2.914a.25.25 0 00-.177-.073H2.75zM1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V1.75zm7 1.5a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0V7h-1.5a.75.75 0 010-1.5h1.5V4A.75.75 0 018 3.25zm-3 8a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75z"></path></svg>
<% if (plugins.lines.error) { %>
<%= plugins.lines.error.message %>
<% } else { %>
<%= plugins.lines.added %> added, <%= plugins.lines.deleted %> removed
<% } %>
</div>
<% } %>
</section>
</div>
</section>
<% } %>
<% if (plugins.followup) { %>
<div class="row">
<section class="column">
<h3>Issues</h3>
<% if (plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.followup.error.message %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= plugins.followup.issues.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
<span class="no-wrap"><%= plugins.followup.issues.closed %> Closed</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.issues.open %> Open</span>
</div>
</div>
<% } %>
</section>
<section class="column">
<h3>Pull requests</h3>
<% if (plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.followup.error.message %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= plugins.followup.pr.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.pr.merged %> Merged</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
<span class="no-wrap"><%= plugins.followup.pr.open %> Open</span>
</div>
</div>
<% } %>
</section>
</div>
<% } %>
<% if (plugins.projects) { %>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
Active projects
</h2>
<div class="row">
<% if (plugins.projects.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.projects.error.message %>
</div>
</section>
<% } else { %>
<section>
<% for (const {name, updated, progress} of plugins.projects.list) { %>
<div class="row fill-width">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
<%= name %>
</div>
</section>
</div>
<div class="row">
<section>
<div class="field">
<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 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Updated <%= updated %>
</div>
</section>
<% if (progress.enabled) { %>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
<%= [progress.done ? `${progress.done} done` : "", progress.doing ? `${progress.doing} doing` : "", progress.todo ? `${progress.todo} todo` : ""].filter(str => str).join(" · ") %>
</div>
</section>
<% } %>
</div>
<% if (progress.enabled) { %>
<div class="field center horizontal-wrap ">
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="project-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#project-bar)" x="0" y="0" width="<%= (progress.done/progress.total)*460 %>" height="8" fill="#28A745"/>
<rect mask="url(#project-bar)" x="<%= (progress.done/progress.total)*460 %>" y="0" width="<%= (progress.doing/progress.total)*460 %>" height="8" fill="#6F42C1"/>
<rect mask="url(#project-bar)" x="<%= ((progress.done+progress.doing)/progress.total)*460 %>" y="0" width="<%= (progress.todo/progress.total)*460 %>" height="8" fill="#d1d5da"/>
</svg>
</div>
<% } %>
<% } %>
</section>
<% } %>
</div>
</section>
<% } %>
<% } %>
<% if (base.metadata) { %>
<footer>
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
</footer>
<% } %>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,60 @@
//Imports
import common from "./../common.mjs"
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Check arguments
const {repo} = q
if (!repo) {
console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`)
data.errors.push({error:{message:`You must pass a "repo" argument to use this template`}})
return await common(...arguments)
}
//Retrieving single repository
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
const {user:{repository}} = await graphql(queries.repository({login, repo}))
data.user.repositories.nodes = [repository]
//Get commit activity
console.debug(`metrics/compute/${login}/${repo} > querying api for commits`)
const commits = []
for (let page = 0; page < 1; page++) {
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page})
if (!data.length) {
console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
break
}
commits.push(...data)
}
console.debug(`metrics/compute/${login}/${repo} > ${commits.length} commits loaded`)
//Override creation date and disk usage
data.user.createdAt = repository.createdAt
data.user.repositories.totalDiskUsage = repository.diskUsage
//Override contributions calendar
const days = 14
//Compute relative date for each contribution
const now = new Date()
now.setHours(0, 0, 0, 0)
const contributions = commits.map(({commit}) => Math.abs(Math.ceil((now - new Date(commit.committer.date))/(24*60*60*1000)))).slice(0, days)
//Count contributions per relative day
const calendar = new Array(days).fill(0)
for (const day of contributions)
calendar[day]++
const max = Math.max(...calendar)
//Override contributions calendar
data.user.calendar.contributionCalendar.weeks = calendar.map(commit => ({contributionDays:{color:commit ? `var(--color-calendar-graph-day-L${Math.ceil(commit/max/0.25)}-bg)` : "var(--color-calendar-graph-day-bg)"}}))
//Override plugins parameters
q["projects.limit"] = 0
//Common
await common(...arguments)
await Promise.all(pending)
//Reformat projects name
data.plugins.projects.list.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim())
}

View File

@@ -1,103 +0,0 @@
query Metrics {
user(login: $login) {
databaseId
name
login
createdAt
avatarUrl
websiteUrl
isHireable
twitterUsername
gists {
totalCount
}
repositories(last: $repositories, isFork: false, ownerAffiliations: OWNER) {
totalCount
totalDiskUsage
nodes {
name
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 4) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
releases {
totalCount
}
forkCount
licenseInfo {
spdxId
}
}
}
packages {
totalCount
}
starredRepositories {
totalCount
}
watching {
totalCount
}
sponsorshipsAsSponsor {
totalCount
}
sponsorshipsAsMaintainer {
totalCount
}
contributionsCollection {
totalRepositoriesWithContributedCommits
totalCommitContributions
restrictedContributionsCount
totalIssueContributions
totalPullRequestContributions
totalPullRequestReviewContributions
}
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
contributionCalendar {
weeks {
contributionDays {
color
}
}
}
}
repositoriesContributedTo {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
issueComments {
totalCount
}
organizations {
totalCount
}
}
}

View File

@@ -2,7 +2,7 @@
import common from "./../common.mjs" import common from "./../common.mjs"
/** Template processor */ /** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) { export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
//Common //Common
await common(...arguments) await common(...arguments)
//Disable optimization to keep white-spaces //Disable optimization to keep white-spaces

View File

@@ -15,6 +15,7 @@
const __src = path.join(__dirname, "src") const __src = path.join(__dirname, "src")
const __plugins = path.join(__src, "plugins") const __plugins = path.join(__src, "plugins")
const __templates = path.join(__src, "templates") const __templates = path.join(__src, "templates")
const __queries = path.join(__src, "queries")
process.on("unhandledRejection", error => { throw error }) process.on("unhandledRejection", error => { throw error })
colors.enable() colors.enable()
@@ -70,7 +71,7 @@
return [`with:`, ...Object.entries({ return [`with:`, ...Object.entries({
token:"${{ secrets.METRICS_TOKEN }}", token:"${{ secrets.METRICS_TOKEN }}",
dryrun:true, dryrun:true,
repositories:1, repositories:0,
template:"${{ matrix.template }}", template:"${{ matrix.template }}",
base:"", base:"",
plugins_errors_fatal:true, plugins_errors_fatal:true,
@@ -109,26 +110,37 @@
console.log(`Generated action`.grey) console.log(`Generated action`.grey)
//Perform assets includes //Perform assets includes
{
const assets = {} const assets = {}
const templates = (await fs.promises.readdir(__templates)).filter(name => !/.*[.]mjs$/.test(name)).sort() const templates = (await fs.promises.readdir(__templates)).filter(name => !/.*[.]mjs$/.test(name)).sort()
for (const name of templates) { for (const name of templates) {
const files = [ const files = [
`${__templates}/${name}/query.graphql`,
`${__templates}/${name}/image.svg`, `${__templates}/${name}/image.svg`,
`${__templates}/${name}/style.css`, `${__templates}/${name}/style.css`,
`${__templates}/${name}/fonts.css`, `${__templates}/${name}/fonts.css`,
] ].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${__templates}/${name}/`, `${__templates}/classic/`))
const [query, image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`)) const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
assets[name] = {query, image, style, fonts} assets[name] = {image, style, fonts}
console.log(`Prepared template ${name}`.grey) console.log(`Prepared template ${name}`.grey)
} }
code = code.replace(/<#assets>/g, Buffer.from(JSON.stringify(assets)).toString("base64")) code = code.replace(/<#assets>/g, Buffer.from(JSON.stringify(assets)).toString("base64"))
console.log(`Included ${templates.length} templates to generated action`.grey) console.log(`Included ${templates.length} templates to generated action`.grey)
}
//Perform queries includes
{
const assets = {}
const queries = (await fs.promises.readdir(__queries)).sort()
code = code.replace(/<#queries>/g, Buffer.from(JSON.stringify(assets)).toString("base64"))
console.log(`Included ${queries.length} queries to generated action`.grey)
}
//Perform version include //Perform version include
{
const version = JSON.parse(await fs.promises.readFile(path.join(__dirname, "package.json"))).version const version = JSON.parse(await fs.promises.readFile(path.join(__dirname, "package.json"))).version
code = code.replace(/<#version>/g, version) code = code.replace(/<#version>/g, version)
console.log(`Included version number (${version}) to generated action`.grey) console.log(`Included version number (${version}) to generated action`.grey)
}
//Minify //Minify
code = minify(code).code code = minify(code).code

View File

@@ -59,14 +59,8 @@ jobs:
<%- testcase({ <%- testcase({
plugin_pagespeed: true, plugin_pagespeed: true,
plugin_pagespeed_token: "${{ secrets.PAGESPEED_TOKEN }}", plugin_pagespeed_token: "${{ secrets.PAGESPEED_TOKEN }}",
}) %>
- name: ${{ matrix.template }} > Plugin > PageSpeed (detailed)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_pagespeed: true,
plugin_pagespeed_detailed: true, plugin_pagespeed_detailed: true,
plugin_pagespeed_token: "${{ secrets.PAGESPEED_TOKEN }}", plugin_pagespeed_screenshot: true,
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Music (playlist - apple) - name: ${{ matrix.template }} > Plugin > Music (playlist - apple)
@@ -99,12 +93,6 @@ jobs:
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Isocalendar - name: ${{ matrix.template }} > Plugin > Isocalendar
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_isocalendar: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Isocalendar (full year)
uses: lowlighter/metrics@<%- release %> uses: lowlighter/metrics@<%- release %>
<%- testcase({ <%- testcase({
plugin_isocalendar: true, plugin_isocalendar: true,
@@ -116,14 +104,6 @@ jobs:
<%- testcase({ <%- testcase({
plugin_habits: true, plugin_habits: true,
plugin_habits_from: 5, plugin_habits_from: 5,
}) %>
- name: ${{ matrix.template }} > Plugin > Habits (charts)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_habits: true,
plugin_habits_from: 5,
plugin_habits_facts: false,
plugin_habits_charts: true, plugin_habits_charts: true,
}) %> }) %>
@@ -139,15 +119,10 @@ jobs:
plugin_followup: true, plugin_followup: true,
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Lines - name: ${{ matrix.template }} > Plugin > Lines and Traffic
uses: lowlighter/metrics@<%- release %> uses: lowlighter/metrics@<%- release %>
<%- testcase({ <%- testcase({
plugin_lines: true, plugin_lines: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Traffic
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_traffic: true, plugin_traffic: true,
}) %> }) %>
@@ -157,38 +132,27 @@ jobs:
plugin_gists: true, plugin_gists: true,
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Topics (stars)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_topics: true,
plugin_topics_sort: "stars",
}) %>
- name: ${{ matrix.template }} > Plugin > Topics (activity)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_topics: true,
plugin_topics_sort: "activity",
}) %>
- name: ${{ matrix.template }} > Plugin > Topics (starred) - name: ${{ matrix.template }} > Plugin > Topics (starred)
uses: lowlighter/metrics@<%- release %> uses: lowlighter/metrics@<%- release %>
<%- testcase({ <%- testcase({
plugin_topics: true, plugin_topics: true,
plugin_topics_sort: "starred", plugin_topics_mode: "starred",
plugin_topics_sort: "random",
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Topics (random) - name: ${{ matrix.template }} > Plugin > Topics (mastered)
uses: lowlighter/metrics@<%- release %> uses: lowlighter/metrics@<%- release %>
<%- testcase({ <%- testcase({
plugin_topics: true, plugin_topics: true,
plugin_topics_sort: "random", plugin_topics_mode: "mastered",
plugin_topics_sort: "stars",
}) %> }) %>
- name: ${{ matrix.template }} > Plugin > Projects - name: ${{ matrix.template }} > Plugin > Projects
uses: lowlighter/metrics@<%- release %> uses: lowlighter/metrics@<%- release %>
<%- testcase({ <%- testcase({
plugin_projects: true, plugin_projects: true,
plugin_projects_repositories: "lowlighter/metrics/projects/1",
plugin_projects_limit: 2, plugin_projects_limit: 2,
}) %> }) %>