diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7372108e..c533c63a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,131 +4,447 @@ Nice! Read the few sections below to understand how this project is structured and how to implement new features. -## ๐ŸŽฌ Behind the scenes - -This section explore some topics which explain globally how metrics was designed and how it works. - -
-๐Ÿ’ฌ Creating SVGs images on-the-fly - -Metrics actually exploit the possibility of integrating HTML and CSS into SVGs, so basically creating these images is as simple as designing static web pages. It can even handle animations and transparency. - -![Metrics are html](.github/readme/imgs/about_metrics_are_html.png) - -SVGs are templated through [EJS framework](https://github.com/mde/ejs) to make the whole rendering process easier thanks to variables, conditional and loop statements. Only drawback is that it tends to make syntax coloration a bit confused because templates are often misinterpreted as HTML tags markers (`<%= "EJS templating syntax" %>`). - -Images (and custom fonts) are encoded into base64 to prevent cross-origin requests, while also removing any external dependencies, although it tends to increase files sizes. - -Since SVG renders differently depending on OS and browsers (system fonts, CSS support, ...), it's pretty hard to compute dynamically height. Previously, it was computed with ugly formulas, but as it wasn't scaling really well (especially since the introduction of variable content length plugins). It was often resulting in large empty blank spaces or really badly cropped image. - -To solve this, metrics now spawns a [puppeteer](https://github.com/puppeteer/puppeteer) instance and directly render SVG in a browser environment (with all animations disabled). An hidden "marker" element is placed at the end of the image, and is used to resize image through its Y-offset. - -![Metrics marker](.github/readme/imgs/about_metrics_marker.png) - -Additional bonus of using pupeeter is that it can take screenshots, making it easy to convert SVGs to PNG output. - -Finally, SVGs image can be optimized through [svgo](https://github.com/svg/svgo), which helps to remove unused attributes and blank space, while also reducing a bit the file size. - -
- -
-๐Ÿ’ฌ Gathering external data from GitHub APIs and Third-Party services - -Metrics mostly use GitHub APIs since it is its primary target. Most of the time, data are retrieved through GraphQL to save APIs requests, but it sometimes fallback on REST for other features. Octokit SDKs are used to make it easier. - -As for other external services (Twitter, Spotify, PageSpeed, ...), metrics use their respective APIs, usually making https requests through [axios](https://github.com/axios/axios) and by following their documentation. It would be overkill to install entire SDKs for these since plugins rarely uses more than 2/3 calls. - -In last resort, pupeeter is seldom used to scrap websites, though its use tends to make things slow and unstable (as it'll break upon HTML structural changes). - -
- -
-๐Ÿ’ฌ Web instance and GitHub action similarities - -Historically, metrics used to be only a web service without any customization possible. The single input was a GitHub username, and was composed of what is now `base` content (along with `languages` and `followup` plugin, which is why they can be computed without any additional queries). That's why `base` content is handled a bit differently from plugins. - -As it gathered more and more plugins over time, generating a single user's metrics was becoming costly both in terms of resources but also in APIs requests. It was thus decided to switch to GitHub Action. At first, it was just a way to explore possibilities of this GitHub feature, but now it's basically the full-experience of metrics (unless you use your own self-hosted instance). - -Both web instance and Action actually use the same entrypoint so they basically have the same features. -Action just format inputs into a query-like object (similarly to when url params are parsed by web instance), from which metrics compute the rendered image. It also makes testing easier, as test cases can be reused since only inputs differs. - -
- -
-๐Ÿ’ฌ Testing and mocking - -Testing is done through [jest](https://github.com/facebook/jest) framework. - -While the best would be to work with real data during testing, to avoid consuming too much APIs requests for testing (and to be more planet friendly), they're [mocked](https://github.com/lowlighter/metrics/blob/master/source/app/mocks.mjs) using [JavaScript Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and [Faker.js](https://github.com/marak/Faker.js/). Basically function calls are "trapped" and send randomly generated data from Faker.js if we're in a development environment. - -
- ## ๐Ÿ‘จโ€๐Ÿ’ป Extending metrics -This section explain how to create new templates and plugins, and contains references about used packages and project structure. -It also lists which contributions on main repository are accepted. +This section explains how to create new templates and plugins. It also contains references about used packages, global project structure and list which contributions on are accepted. -
-๐Ÿค Accepted contributions +### ๐Ÿค Accepted contributions -Thanks for wanting to help metrics growing! +Thanks for your interest in [metrics](https://github.com/lowlighter/metrics) and wanting to help it growing! Review below which contributions are accepted: - - - + + - + - + - + - + - +
SectionExamplesAdditionEditionsAdditionEditions
๐Ÿงฉ Plugins โœ”๏ธ โœ”๏ธ +
    +
  • New plugins are welcomed provided they're not redundant with existing plugins
  • +
  • New features for existing plugins are allowed but must be optional
  • +
+
๐Ÿ–ผ๏ธ Templates โŒ โญ• +
    +
  • New templates are not allowed (use ๐Ÿ“• Community templates instead)
  • +
  • Templates editions are allowed for new features additions (but must remain consistent with current visuals)
  • +
+
๐Ÿงช Teststests/metrics.test.js โœ”๏ธ โœ”๏ธ +
    +
  • Everything that make metrics more stable is welcomed!
  • +
+
๐Ÿงฑ Coreapp/metrics/, Dockerfile, package.json ... โŒ โญ• +
    +
  • Core editions impacts all rendering process and should be avoided unless necessary
  • +
+
๐Ÿ—ƒ๏ธ Repository.github/, LICENSE, CONTRIBUTING.md, ... โŒ โŒ +
    +
  • Workflows, license, readmes, etc. usually don't need to be edited
  • +
+
**Legend** * โœ”๏ธ: Contributions welcomed! -* โญ•: Contributions welcomed, but must be discussed first with a maintainer +* โญ•: Contributions welcomed, but must be discussed first * โŒ: Only maintainers can manage these files -Before working on something, ensure that it isn't listed in [In progress](https://github.com/lowlighter/metrics/projects/1#column-12158618) and that no open pull requests (including drafts) already implement what you want to do. +Before working on something, ensure that it isn't already [in progress](https://github.com/lowlighter/metrics/projects/1#column-12158618) and that it will not duplicate any open pull requests (including drafts). +If you're unsure, always open an issue first to get insights and gather feedback. -If it's listed in [Roadmap and todos](https://github.com/lowlighter/metrics/projects/1) be sure to let maintainers that you're working on it. As metrics remains a side project, things being working on can change from one day to another. - -If you're unsure, always open an issue to obtain insights and feedback ๐Ÿ™‚ - -And even if your changes don't get merged in [lowlighter/metrics](https://github.com/lowlighter/metrics), please don't be too sad. -Metrics is designed to be highly customizable, so you can always decide to generate metrics on your forked repository! +Even if your changes don't get merged in [lowlighter/metrics](https://github.com/lowlighter/metrics), please don't be too sad. +Metrics is designed to be highly customizable, so you can always decide to generate metrics on your forked repository ๐Ÿ™‚!
+
+๐Ÿ–ผ๏ธ Templates + +Templates require you to be comfortable with HTML, CSS and JavaScript ([EJS](https://github.com/mde/ejs) flavored). + +Metrics does not really accept contributions on [default templates](https://github.com/lowlighter/metrics/tree/master/source/templates) in order to avoid bloating main repository with a lot of templates and to keep visual consistency across all version, but fear not! Users will still be able to use your custom templates thanks to [community templates](source/templates/community)! + +If you make something awesome, don't hesitate to share it! + +
+๐Ÿ’ฌ Creating a new template from scratch + +Find a cool name for your template and run: +```shell +npm run quickstart -- template +``` + +It will create a new folder in [`source/templates`](https://github.com/lowlighter/metrics/tree/master/source/templates) with the following files: +- A `README.md` to describe your template and document it +- An `image.svg` with base structure for rendering +- A `partials/` folder where you'll be able to implement parts of your template + - A `partials/_.json` with a JSON array listing these parts in the order you want them displayed (unless overridden by user with `config_order` option) + +If needed, you can also create the following optional files: +- A `fonts.css` containing base64 encoded custom fonts +- A `styles.css` with custom CSS that'll style your template +- A `template.mjs` with additional data processing and formatting at template-level + - When your template is used through `setup_community_templates` on official releases, this is disabled by default unless user trusts it by appending `+trust` at the end of source + +If inexistent, these will fallback to [`classic`](https://github.com/lowlighter/metrics/tree/master/source/templates/classic) template files. + +Templates are auto-loaded based on their folder existence, so there's no need to register them somewhere. + +
+ +
+๐Ÿ’ฌ Creating image.svg and partials + +The base structure for rendering looks like below: +```html + + + + + + +
+ <% for (const partial of [...partials]) { %> + <%- await include(`partials/${partial}.ejs`) %> + <% } %> + +
+
+
+ +
+``` + +- `fonts` and `style` variables will both be populated with the same content as your `fonts.css` and `styles.css` files + - (or thos of `classic` template files if inexistent) +- `partials` variable will be populated with `partials/_.json` content + - Main loop will iterate over this array to include all defined partials +- `#metrics-end` is a special HTML tag which must remain at the bottom of SVG template + - This is used to compute dynamically height through a [puppeteer](https://github.com/puppeteer/puppeteer) headless instance + - SVG height must also be set to a high number so it doesn't get cropped accidentally while [puppeteer](https://github.com/puppeteer/puppeteer) compute [element.getBoundingClientRect()](https://developer.mozilla.org/fr/docs/Web/API/Element/getBoundingClientRect) + +As you can see, we exploit the fact that SVG images are able to render HTML and CSS content so designing partials is the same as creating static web pages. + +[EJS](https://github.com/mde/ejs) framework is also used to programmatically create content through the help of templating tags (`<% %>`). + +
+ +
+๐Ÿ’ฌ Adding custom fonts + + โš ๏ธ This significantly increases rendered metrics filesize and thus not recommended + +When using this feature, you should aim to restrict used charset to avoid including useless data. + +Here's a quick step-by-step tutorial to create base64 encoded 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 `href` +- 2. Open extracted `href` in a browser and append `&text=` parameter with list of used characters + - e.g. `&text=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` +- 3. Download each font file from urls present in generated stylesheet +- 4. Convert them into base64 with `woff` format on [transfonter.org](https://transfonter.org) +- 5. Download archive and extract it +- 6. Copy content of generated stylesheet to `fonts.css` +- 7. Update your template `style.css` to use the new font + +
+ +
+ + +
+๐Ÿงฉ Plugins + +Plugins lets add new features with additional content to rendered metrics and are coded with [JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules). + +New plugins are welcomed, but maintainers have no obligation to maintain them. +It means you (as author) may be notified about open issues regarding related plugin. + +
+๐Ÿ’ฌ Creating a new plugin from scratch + +Find a cool name for your plugin and run: +```shell +npm run quickstart -- plugin +``` + +It will create a new folder in [`source/plugins`](https://github.com/lowlighter/metrics/tree/master/source/plugins) with the following files: +- A `README.md` to describe your plugin and document it +- An `index.mjs` with minimal plugin code +- A `metadata.yml` which list plugin attributes and inputs +- A `tests.yml` for unit tests + +Here are some guidelines to follow about plugins: +- They should never be dependent on output produced by other plugins (though allowed to re-use core and base data) + - It allows parallelization of plugins execution + - It avoids creating inter-dependencies which makes it confusing for both developers and users +- Use of new external dependencies should be avoided + - Adding new libraries to use only ~5% of its possibilities is just a waste + - For APIs, most of the time a few HTTP calls instead of installing a full SDK wrapper is more than sufficient + - `imports` probably already contains a library or a function that can help you achieving what you want + - It also add more unstability as it external changes are +- Use of raw commands should be avoided when (spawning sub-process) + - It allows metrics to be platform agnostic (i.e. working on most OS) + - If mandatory: + - Use [`which`](https://linux.die.net/man/1/which) detect whether command is available + - For Windows, wrap command with [WSL](https://docs.microsoft.com/windows/wsl/about) +- Errors should be handled gracefully with error messages +- Plugins arguments should **NEVER** be directly edited from inside a plugin + - These are used by all plugins, including core and base so it would create unattended side effects +- They should let end user with some customization options (limit entries, detailed output, etc.) + +You'll also need to an unused [emoji](https://emojipedia.org) to use as your plugin icon. + +Plugins are auto-loaded based on their folder existence, so there's no need to register them somewhere. + +
+ +
+๐Ÿ’ฌ Implementing index.mjs and gathering new data from external APIs + +Default exported function in `index.mjs` will receive the following inputs: +- `login`, set to GitHub login +- `q`, with query parameters (formatted with dots (`.`) instead of underscores (`_`) and without `plugin_` prefix) +- `imports`, with libraries and utilitaries + - `imports.url` for [NodeJS `url` library](https://nodejs.org/api/url.html) + - `imports.os` for [NodeJS `os` library](https://nodejs.org/api/os.html) + - `imports.fs` for [NodeJS `fs` library](https://nodejs.org/api/fs.html) + - `imports.paths` for [NodeJS `paths` library](https://nodejs.org/api/paths.html) + - `imports.util` for [NodeJS `util` library](https://nodejs.org/api/util.html) + - `imports.imgb64` for [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64) + - `imports.axios` for [axios/axios](https://github.com/axios/axios) + - `imports.puppeteer` for [puppeteer/puppeteer](https://github.com/puppeteer/puppeteer) + - `imports.run` is an helper to run raw command + - `imports.shuffle` is an helper to shuffle array + - `imports.__module` is an helper to find `__dirname` from a module `import.meta.url` + - And more... +- `data` and `computed`, with all data gathered from core and base +- `graphql` and `rest`, with authenticated [octokit clients](https://github.com/octokit) (for GitHub APIs) +- `queries`, with autoloaded GraphQL queries and replacers +- `account`, set to account type ("user" or "organization") + +Second input contains configuration settings from [settings.json](https://github.com/lowlighter/metrics/blob/master/settings.example.json) (which is mostly used by web instances) and all also user inputs of type `token`. + +As said previously, plugins arguments should **NEVER** be directly edited from it, since these are used by all plugins, including core and base so it would create unattended side effects. + +As for data gathering: + - Related to GitHub, use `graphql` (for [GraphQL API](https://docs.github.com/en/graphql)) or `rest` [REST API](https://docs.github.com/en/rest) + - From Third-Party services, use [`imports.axios`](https://github.com/axios/axios) to make APIs calls + - In last resort, use `imports.puppeteer` + +For GraphQL queries, use `queries` which will auto-load all queries from `queries` directory and will lets you create custom queries on the fly. + +For example: +```js +//Calling this + await graphql(queries.myquery({login:"github-user", account:"user"})) + +//With this in source/queries/myquery.graphql + query MyQuery { + $account(login: "$login") { + name + } + } + +//Will have the same result as calling this + await graphql(` + query MyQuery { + user(login: "github-user") { + name + } + } + `) +``` + +
+ + +
+๐Ÿ’ฌ Filling metadata.yml + +`metadata.yml` is a mandatory file which describes what inputs are allowed, which entities are supported, etc. + +Here's an example: +```yaml +name: "๐Ÿงฉ Plugin name (with emoji icon)" +cost: Estimates how many GitHub requests is used during plugin execution ("N/A" for Third-Party services) +supports: + - user # Support users account + - organization # Support organizations account + - repository # Support repositories metrics +inputs: + + # A comment detailing input purposes + # An input must have at least a "description" and a "default" (used to generated GitHub Action `action.yml`) + plugin_input: + description: Short description (few words) + type: boolean + default: no +``` + +Because of GitHub Actions limitations, only strings and numbers are actually supported by `action.yml` inputs. +Metrics apply additional post-processing to handle inputs. + +Supported input types are `boolean`, `string`, `number`, `array` and `json`. + +- Allowed values for `string` and `array` may be restricted using `values` attribute + - Special default values `.user.login`, `.user.twitter` and `.user.website` will respectively be replaced by user's login, Twitter username and website (not available when `token` is set to `NOT_NEEDED` by user ) +- Lower and upper limits for `number` may be set using `min` and `max` attribute +- Array `format` attribute define how string should be splitted (`comma-separated` or `space-separated`) + +You can additionally specify an `example` which will also be used in web instance input placeholder. + +Inputs will be available through `imports.metadata.plugins.name.inputs` with correct typing and default values (`plugin_` prefix will be dropped, and all underscored (`_`) will be changed to dots (`.`) instead): +```javascript +//Load inputs + let {limit, "limit.field":limit_field} = imports.metadata.plugins.name.inputs({data, account, q}) +``` + +Additionally, if `account` isn't supported, this method will automatically prevent your plugin from running by throwing an error. + +
+ +
+๐Ÿ’ฌ Creating a new partial + +In templates you want to support, create a new `.ejs` file in `partials` folder and paste the following for a quick start: +```html +<% if (plugins./* your plugin name */) { %> +
+
+ <% if (plugins./* your plugin name */.error) { %> +
+
+ + <%= plugins./* your plugin name */.error.message %> +
+
+ <% } else { %> +
+ <%# Do stuff in there -%> +
+ <% } %> +
+
+<% } %> +``` + +- First conditional statement ensures that partial is displayed only when plugin is enabled +- Nested conditional statement check plugin output + - If it failed, an error message instead will be displayed instead + - If it succeeded, second section in render. + +Additional CSS rules may be added to `style.css` of edited template, but ensure it does not break other plugins rendering. + +
+ + +
+๐Ÿ’ฌ Fast prototyping and testing + +The easiest way to test and prototype your plugin is to setup a web instance. See [documentation](https://github.com/lowlighter/metrics#%EF%B8%8F-deploying-your-own-web-instance-15-min-setup-depending-on-your-sysadmin-knowledge) for more informations about that. + +Open a browser and try to generate metrics with new your plugin enabled to see if it works as expected: +``` +http://localhost:3000/your-github-login?base=0&your-plugin-name=1 +``` + +Once ready, define test cases in your plugin directory `tests.yml`. + +These tests will be run with: + - Metrics action + - Metrics web instance + - Metrics web instance placeholder (rendered by browser) + +Most APIs (including GitHub) usually have a rate-limit to ensure quality of service. +This is why APIs output must be mocked and added in [`source/app/mocks/api/`](/source/app/mocks/api) in order for tests to be able to be performed anytime. + +Files from these directories are auto-loaded, so you just need to create new functions (see other mocked data for examples). + +Finally, edit [source/app/web/statics/app.placeholder.js](https://github.com/lowlighter/metrics/blob/master/source/app/web/statics/app.placeholder.js) to add mocked result (but this time from metrics server) so users will be able to render placeholder preview in web instance. + +
+ +
+๐Ÿ’ฌ Submitting a pull request + +Ensure that: +- `metadata.yml` is correctly filled +- `tests.yml` has defined test cases +- `mocks/api` has mocked data for external APIs +- `app.placeholder.js` has been updated with mocked plugin output +- `README.md` of plugin explain how plugin works + - `` tag **MUST** remain present (along with ``) as these are extracted for global `README.md` +- `npm run linter` outputs no errors +- `npm test` is successful + +Use `config.output` option to render a PNG version of your plugin: +``` +http://localhost:3000/your-github-login?base=0&your-plugin-name=1&config.output=png +``` + +And finally open a new [pull request](https://github.com/lowlighter/metrics/pulls) and ensure that all builds succeed. + +Global `README.md`, `plugins/README.md`, `templates/README.md`, `action.yml` and `settings.example.json` are automatically rebuild by GitHub action, do not edit them manually. + +```markdown +### ๐Ÿงฉ Your plugin name + +
+ +
+ + +
+ +#### โ„น๏ธ Examples workflows + +[โžก๏ธ Available options for this plugin](metadata.yml) + +'''yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_custom: yes +''' + +``` + +Note that you **must** keep . + +
+ +
๐Ÿ—‚๏ธ Project structure @@ -206,433 +522,62 @@ Below is a list of used packages.
+## ๐ŸŽฌ Behind the scenes + +This section explore some topics which explain globally how metrics was designed and how it works.
-๐Ÿ–ผ๏ธ Templates +๐Ÿ’ฌ Creating SVGs images on-the-fly -Templates requires you to be comfortable with HTML, CSS and JavaScript ([EJS](https://github.com/mde/ejs) flavored). +Metrics actually exploit the possibility of integrating HTML and CSS into SVGs, so basically creating these images is as simple as designing static web pages. It can even handle animations and transparency. -Metrics does not really accept contributions on [default templates](https://github.com/lowlighter/metrics/tree/master/source/templates) in order to avoid bloating main repository with a lot of templates, but fear not! Users will still be able to use your custom templates thanks to [community templates](source/templates/community)! +![Metrics are html](.github/readme/imgs/about_metrics_are_html.png) -If you make something awesome, don't hesistate to share it! +SVGs are templated through [EJS framework](https://github.com/mde/ejs) to make the whole rendering process easier thanks to variables, conditional and loop statements. Only drawback is that it tends to make syntax coloration a bit confused because templates are often misinterpreted as HTML tags markers (`<%= "EJS templating syntax" %>`). -For a quick start, use: -```shell -npm run quickstart -- template -``` +Images (and custom fonts) are encoded into base64 to prevent cross-origin requests, while also removing any external dependencies, although it tends to increase files sizes. -
-๐Ÿ’ฌ Creating a new template from scratch +Since SVG renders differently depending on OS and browsers (system fonts, CSS support, ...), it's pretty hard to compute dynamically height. Previously, it was computed with ugly formulas, but as it wasn't scaling really well (especially since the introduction of variable content length plugins). It was often resulting in large empty blank spaces or really badly cropped image. -Find a cool name for your template and create an eponym folder in [`source/templates`](https://github.com/lowlighter/metrics/tree/master/source/templates). +To solve this, metrics now spawns a [puppeteer](https://github.com/puppeteer/puppeteer) instance and directly render SVG in a browser environment (with all animations disabled). An hidden "marker" element is placed at the end of the image, and is used to resize image through its Y-offset. -Then, you'll need to create the following files: -- `README.md` will contain template description and documentation -- `image.svg` will contain the base render structure of your template -- `partials/` is a folder that'll contain parts of your template (called "partials") - - `partials/_.json` is a JSON array which lists your partials (these will be displayed in the same order as listed, unless if overriden by user with `config_order` option) +![Metrics marker](.github/readme/imgs/about_metrics_marker.png) -The following files are optional: -- `fonts.css` can contain your custom fonts (base64 encoded) if needed -- `styles.css` can contain your CSS that'll style your template -- `template.mjs` can contain additional data processing and formatting at template-level +Additional bonus of using pupeeter is that it can take screenshots, making it easy to convert SVGs to PNG output. -Optional files will fallback to the one defined in [`classic`](https://github.com/lowlighter/metrics/tree/master/source/templates/classic) template if unexistant. - -Note that by default, `template.mjs` is skipped when using official release with community templates, to prevent malicious code to leaks token and credentials. +Finally, SVGs image can be optimized through [svgo](https://github.com/svg/svgo), which helps to remove unused attributes and blank space, while also reducing a bit the file size.
-๐Ÿ’ฌ Creating a README.md +๐Ÿ’ฌ Gathering external data from GitHub APIs and Third-Party services -Your `README.md` will document your template and explain how it works. -It must contain at least the following: -```markdown -### ๐Ÿ“• My custom template +Metrics mostly use GitHub APIs since it is its primary target. Most of the time, data are retrieved through GraphQL to save APIs requests, but it sometimes fallback on REST for other features. Octokit SDKs are used to make it easier. - - -
- - -
+As for other external services (Twitter, Spotify, PageSpeed, ...), metrics use their respective APIs, usually making https requests through [axios](https://github.com/axios/axios) and by following their documentation. It would be overkill to install entire SDKs for these since plugins rarely uses more than 2/3 calls. -#### โ„น๏ธ Examples workflows - -'''yaml -- uses: lowlighter/metrics@latest - with: - # ... other options - setup_community_templates: user/metrics@master:template - template: "@template" -''' - -``` - -
- - -
-๐Ÿ’ฌ Creating image.svg - -Once you finished setting up template folder structure, paste the following in `image.svg` to get started: -```html - - - - - - -
- <% for (const partial of [...partials]) { %> - <%- await include(`partials/${partial}.ejs`) %> - <% } %> - -
-
-
- -
-``` - -Let's explain what it does. - -`fonts` and `style` variables will be populated with the same content as your `fonts.css` and `styles.css` files. -Like said previously, if these does not exists, it'll contain the same content as the `classic` template files. - -The main loop will iterate on `partials` variable which contains your partials index set in `_.json`. - -Finally, you may have noticed that `height` is set to a very high number, and that there is a `#metrics-end` element at the bottom of the SVG template. This is because rendered height is computed dynamically through a [puppeteer](https://github.com/puppeteer/puppeteer) browser instance which locate `#metrics-end` and use its *y-coordinate* and `config_padding` to set final height. So you should leave it like this to ensure your rendered image will be correctly sized. +In last resort, pupeeter is seldom used to scrap websites, though its use tends to make things slow and unstable (as it'll break upon HTML structural changes).
-๐Ÿ’ฌ Customizing templates with partials +๐Ÿ’ฌ Web instance and GitHub action similarities -Partials are sections that'll be displayed in rendered metrics. +Historically, metrics used to be only a web service without any customization possible. The single input was a GitHub username, and was composed of what is now `base` content (along with `languages` and `followup` plugin, which is why they can be computed without any additional queries). That's why `base` content is handled a bit differently from plugins. -It's just HTML with CSS which can be templated through [EJS](https://github.com/mde/ejs) framework. -Basically, you can use JavaScript statements in templating tags (`<% %>`) to display variables content and to programmatically create content. +As it gathered more and more plugins over time, generating a single user's metrics was becoming costly both in terms of resources but also in APIs requests. It was thus decided to switch to GitHub Action. At first, it was just a way to explore possibilities of this GitHub feature, but now it's basically the full-experience of metrics (unless you use your own self-hosted instance). + +Both web instance and Action actually use the same entrypoint so they basically have the same features. +Action just format inputs into a query-like object (similarly to when url params are parsed by web instance), from which metrics compute the rendered image. It also makes testing easier, as test cases can be reused since only inputs differs.
-๐Ÿ’ฌ Using custom fonts +๐Ÿ’ฌ Testing and mocking -This is actually not recommended because it drastically increases the size of generated metrics, but it should also make your rendering more consistant. The trick is to actually restrict the charset used to keep file size small. +Testing is done through [jest](https://github.com/facebook/jest) framework. -Below is a simplified process on how to generate base64 encoded fonts to use in 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 `fonts.css` -- 6. Update your template - - Include `` to your `image.svg` - - Edit your `style.css` to use yout new font - -
- -
- - -
-๐Ÿงฉ Plugins - -Plugins are self-sufficient and independant code functions that gather additional data from GitHub APIs or external sources. - -
-๐Ÿ’ฌ Plugin guidelines - -- A plugin should never be dependent on others plugins - - But they're allowed to use data gathered by main metrics function -- Avoid the need of new external dependencies (like SDKs) - - Most of the time, SDKs are overkill when a few HTTP calls do the trick - - `imports` probably contains a library that can help you achieving what you want -- Avoid using raw command when possible (like spawning sub-process) - - Sub-process should be platform agnostic (i.e. working on most OS) -- Errors should always be handled gracefully by displaying an error message when it fails - - When possible, try to display explicit error messages - -
- -For a quick start, use: -```shell -npm run quickstart -- plugin -``` - -
-๐Ÿ’ฌ Creating a new plugin - -Find a cool word to name your plugin and create an eponym folder in [`source/plugins`](https://github.com/lowlighter/metrics/tree/master/source/plugins) folder. - -You'll also need to find an unused [emoji](https://emojipedia.org) that you'll be able to use as your plugin icon. - -Then create an `index.mjs` in your plugin folder and paste the following code: -```js -//Setup - export default async function ({login, q, imports, data, computed, rest, graphql, queries, account}, {enabled = false} = {}) { - //Plugin execution - try { - //Check if plugin is enabled and requirements are met - if ((!enabled)||(!q/* your plugin name */)) - return null - //Results - return {} - } - //Handle errors - catch (error) { - throw {error:{message:"An error occured", instance:error}} - } - } -``` - -The following inputs are available: -- `login` is set to GitHub login -- `q` contains all query parameters -- `imports` contains libraries and utilitaries that are shared amongst plugins - - `imports.url` refers to [NodeJS `url` library](https://nodejs.org/api/url.html) - - `imports.os` refers to [NodeJS `os` library](https://nodejs.org/api/os.html) - - `imports.fs` refers to [NodeJS `fs` library](https://nodejs.org/api/fs.html) - - `imports.paths` refers to [NodeJS `paths` library](https://nodejs.org/api/paths.html) - - `imports.util` refers to [NodeJS `util` library](https://nodejs.org/api/util.html) - - `imports.imgb64` refers to [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64) - - `imports.axios` refers to [axios/axios](https://github.com/axios/axios) - - `imports.puppeteer` refers to [puppeteer/puppeteer](https://github.com/puppeteer/puppeteer) - - `imports.run` is an helper to run raw command - - `imports.shuffle` is an helper to shuffle array - - `imports.__module` is an helper to find `__dirname` from a module url - - And more... -- `data` and `computed` contains all data (and computed data) gathered by various APIs from main metrics function -- `graphql` and `rest` contains [octokit clients](https://github.com/octokit) for GitHub API -- `queries` contains autoloaded GraphQL queries with replacers -- `account` contains the type of account being worked on ("user" or "organization") - -The second input contains configuration settings from [settings.json](https://github.com/lowlighter/metrics/blob/master/settings.example.json), which is mostly used by web instances. - -Content of these parameters should **never** be edited directly, as your plugin should only return a new result. - -Plugins are autoloaded so you do not need to do anything special to register them. - -
- -
-๐Ÿ’ฌ Gathering new data from GitHub APIs and from Third-Party services - -For GitHub related data, always try to use their [GraphQL API](https://docs.github.com/en/graphql) or their [REST API](https://docs.github.com/en/rest) when possible. Use `puppeteer` in last resort. - -When using GraphQL API, `queries` object autoloads queries from your plugin `queries` directory and will replace all strings prefixed by a dollar sign (`$`) with eponym variables. - -For example: -```js -//Calling this - await graphql(queries.myquery({login:"github-user", account:"user"})) - -//With this in source/queries/myquery.graphql - query MyQuery { - $account(login: "$login") { - name - } - } - -//Will have the same result as calling this - await graphql(` - query MyQuery { - user(login: "github-user") { - name - } - } - `) -``` - -For REST API, check out their [documentation](https://octokit.github.io/rest.js/v18/). - -As for Third-Party services, always prefer using their APIs (you can use [`imports.axios`](https://github.com/axios/axios) for easy HTTP requests) when they exists before having recourse to [`imports.puppeteer`](https://github.com/puppeteer/puppeteer). - -New external dependencies should be avoided at all costs, especially since most of the time it's overkill to setup a new SDK. - -
- -
-๐Ÿ’ฌ Creating a partial to display your plugin result - -Create new files in `partials` of `source/templates` you want to support with `.ejs` extension. - -You can paste the following for a quick start: -```html -<% if (plugins./* your plugin name */) { %> -
-
- <% if (plugins./* your plugin name */.error) { %> -
-
- - <%= plugins./* your plugin name */.error.message %> -
-
- <% } else { %> -
- <%# Do stuff in there -%> -
- <% } %> -
-
-<% } %> -``` - -Let's explain what it does. - -First conditional statement will ensure that your partial only execute when your plugin is enabled. - -The nested one will check if your plugin resulted in an error, and if that's the case, it'll display an error message instead. -Else, if it's successful, you'll get the second section in render. - -Plugins errors should always be handled gracefully when possible. - -If you need additional CSS rules, edits the `style.css` of edited template. - -
- -
-๐Ÿ’ฌ Fast prototyping with web instance - -The easiest way to test and prototype your plugin is to use a web instance. - -Configure a [settings.json](https://github.com/lowlighter/metrics/blob/master/settings.example.json) with a valid GitHub token and with debug mode enabled. -Then start a web instance with `npm start` (you may have to run `npm install` if that's the first time you use the web instance). - -Then try to generate your metrics in your browser with your GitHub user and your plugin enabled, and see if it works as expected: -``` -http://localhost:3000/your-github-login?base=0&your-plugin-name=1 -``` - -
- -
-๐Ÿ’ฌ Registering plugin options in metadata.yml - -`metadata.yml` is a special file that will be used to parse user inputs and to generate final `action.yml` - -```yaml -name: "๐Ÿงฉ Your plugin name" - -# Estimate of how many GitHub requests will be used -cost: N/A - -# Supported modes -supports: - - user - - organization - - repository - -# Inputs list -inputs: - - # Enable or disable plugin - plugin_custom: - description: Your custom plugin - type: boolean - default: no -``` - -The following types are supported: -```yaml -string: - type: string - -select: - type: string - values: - - allowed-value-1 - - allowed-value-2 - - ... - -boolean: - type: boolean - -number: - type: number - -ranged: - type: number - min: 0 - max: 100 - -array: - type: array - format: comma-separated - -array_select: - type: array - format: comma-separated - values: - - allowed-value-1 - - allowed-value-2 - - ... - -json: - type: json -``` - -
- -
-๐Ÿ’ฌ Create mocked data and tests - -Creating tests for your plugin ensure that external changes don't break it. - -You can define your tests cases in `tests.yml` in your plugin directory, which will automatically test your plugin with: - - Metrics action - - Metrics web instance - - Metrics web instance placeholder (rendered by browser) - -As most of APIs (including GitHub) usually have a rate-limit to ensure quality of their service. -To bypass these restrictions but still perform tests, you must mock their data which simulates APIs call returns. - -Add them in [`source/app/mocks/api/`](/source/app/mocks/api) folder. - -If you're using `axios` or GitHub GraphQL API, these files are autoloaded so you just need to create new functions (see other mocked data for examples). - -If you're using GitHub REST API, add your mocks in [`source/app/mocks/rest`](/source/app/mocks/rest) with same path as octokit. - -
- -
-๐Ÿ’ฌ Creating a README.md - -Your `README.md` will document your plugin and explain how it works. -It must contain at least the following: - -```markdown -### ๐Ÿงฉ Your plugin name - - - -
- - -
- -#### โ„น๏ธ Examples workflows - -[โžก๏ธ Available options for this plugin](metadata.yml) - -'''yaml -- uses: lowlighter/metrics@latest - with: - # ... other options - plugin_custom: yes -''' - -``` - -Note that you **must** keep `` tags as these will be extracted to autogenerated global `README.md` with your example. - - +While the best would be to work with real data during testing, to avoid consuming too much APIs requests for testing (and to be more planet friendly), they're [mocked](https://github.com/lowlighter/metrics/blob/master/source/app/mocks.mjs) using [JavaScript Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and [Faker.js](https://github.com/marak/Faker.js/). Basically function calls are "trapped" and send randomly generated data from Faker.js if we're in a development environment. diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs index ae680a08..c0dc2bab 100644 --- a/source/app/metrics/utils.mjs +++ b/source/app/metrics/utils.mjs @@ -117,7 +117,9 @@ console.debug(`metrics/svgresize > padding width*${padding.width}, height*${padding.height}`) //Render through browser and resize height const page = await svgresize.browser.newPage() - await page.setContent(svg, {waitUntil:"load"}) + await page.setContent(svg, {waitUntil:["load", "domcontentloaded", "networkidle2"]}) + await page.addStyleTag({content:"body { margin: 0; padding: 0; }"}) + await wait(1) let mime = "image/svg+xml" let {resized, width, height} = await page.evaluate(async padding => { //Disable animations diff --git a/source/plugins/activity/metadata.yml b/source/plugins/activity/metadata.yml index 7f2fc465..69b9bb25 100644 --- a/source/plugins/activity/metadata.yml +++ b/source/plugins/activity/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ“ฐ Recent activity" cost: 1 REST request per 100 events +categorie: github supports: - user - organization diff --git a/source/plugins/anilist/metadata.yml b/source/plugins/anilist/metadata.yml index 37efcfbf..195592aa 100644 --- a/source/plugins/anilist/metadata.yml +++ b/source/plugins/anilist/metadata.yml @@ -1,5 +1,6 @@ name: "๐ŸŒธ Anilist" cost: N/A +categorie: social supports: - user - organization diff --git a/source/plugins/base/README.md b/source/plugins/base/README.md index a2c0c2b7..ab8acaac 100644 --- a/source/plugins/base/README.md +++ b/source/plugins/base/README.md @@ -32,7 +32,8 @@ These are all enabled by default, but you can explicitely opt out from them. - uses: lowlighter/metrics@latest with: # ... other options - base: header, repositories # Only display "header" and "repositories" sections - repositories: 100 # Query only last 100 repositories - repositories_forks: no # Don't include forks + base: header, repositories # Only display "header" and "repositories" sections + repositories: 100 # Query only last 100 repositories + repositories_forks: no # Don't include forks + repositories_affiliations: owner # Display only repositories where user is owner ``` diff --git a/source/plugins/base/index.mjs b/source/plugins/base/index.mjs index 00427100..07b1426f 100644 --- a/source/plugins/base/index.mjs +++ b/source/plugins/base/index.mjs @@ -7,7 +7,7 @@ export default async function({login, graphql, data, q, queries, imports}, conf) { //Load inputs console.debug(`metrics/compute/${login}/base > started`) - let {repositories, repositories_forks:forks} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100}) + let {repositories, "repositories.forks":forks, "repositories.affiliations":affiliations} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100}) //Skip initial data gathering if not needed if (conf.settings.notoken) @@ -23,7 +23,7 @@ try { //Query data from GitHub API console.debug(`metrics/compute/${login}/base > account ${account}`) - const queried = await graphql(queries.base[account]({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks:forks ? "" : ", isFork: false"})) + const queried = await graphql(queries.base[account]({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks:forks ? "" : ", isFork: false", affiliations:affiliations ? `, ownerAffiliations: ${affiliations.toLocaleUpperCase()}` : ""})) Object.assign(data, {user:queried[account]}) postprocess?.[account]({login, data}) //Query repositories from GitHub API @@ -33,7 +33,7 @@ let pushed = 0 do { console.debug(`metrics/compute/${login}/base > retrieving repositories after ${cursor}`) - const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.base.repositories({login, account, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:100, organization:25}[account]), forks:forks ? "" : ", isFork: false"})) + const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.base.repositories({login, account, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:100, organization:25}[account]), forks:forks ? "" : ", isFork: false", affiliations:affiliations ? `, ownerAffiliations: ${affiliations.toLocaleUpperCase()}` : ""})) cursor = edges?.[edges?.length-1]?.cursor data.user.repositories.nodes.push(...nodes) pushed = nodes.length diff --git a/source/plugins/base/metadata.yml b/source/plugins/base/metadata.yml index 97bfd60b..c6f32ed1 100644 --- a/source/plugins/base/metadata.yml +++ b/source/plugins/base/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ—ƒ๏ธ Base content" cost: 1 GraphQL request +categorie: core supports: - user - organization @@ -32,3 +33,13 @@ inputs: description: Include forks in metrics type: boolean default: no + + # Filter repositories by user affiliations + repositories_affiliations: + description: Repositories affiliations + type: string + default: "" + values: + - owner + - collaborator + - organization_member diff --git a/source/plugins/base/queries/repositories.graphql b/source/plugins/base/queries/repositories.graphql index b2b4984c..cf1ec688 100644 --- a/source/plugins/base/queries/repositories.graphql +++ b/source/plugins/base/queries/repositories.graphql @@ -1,6 +1,6 @@ query BaseRepositories { $account(login: "$login") { - repositories($after first: $repositories $forks, orderBy: {field: UPDATED_AT, direction: DESC}) { + repositories($after first: $repositories $forks $affiliations, orderBy: {field: UPDATED_AT, direction: DESC}) { edges { cursor } diff --git a/source/plugins/base/queries/user.graphql b/source/plugins/base/queries/user.graphql index 19eca20a..667ee62d 100644 --- a/source/plugins/base/queries/user.graphql +++ b/source/plugins/base/queries/user.graphql @@ -8,7 +8,7 @@ query BaseUser { websiteUrl isHireable twitterUsername - repositories(last: 0 $forks) { + repositories(last: 0 $forks $affiliations) { totalCount totalDiskUsage nodes { diff --git a/source/plugins/core/metadata.yml b/source/plugins/core/metadata.yml index 415da242..a6eb243f 100644 --- a/source/plugins/core/metadata.yml +++ b/source/plugins/core/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿงฑ Core" cost: N/A +categorie: core supports: - user - organization diff --git a/source/plugins/followup/metadata.yml b/source/plugins/followup/metadata.yml index 73e3e28f..1dd6f6f5 100644 --- a/source/plugins/followup/metadata.yml +++ b/source/plugins/followup/metadata.yml @@ -1,5 +1,6 @@ name: "๐ŸŽŸ๏ธ Follow-up of issues and pull requests" cost: 0 API request +categorie: github supports: - user - organization diff --git a/source/plugins/gists/metadata.yml b/source/plugins/gists/metadata.yml index b7d7ee64..09d22aec 100644 --- a/source/plugins/gists/metadata.yml +++ b/source/plugins/gists/metadata.yml @@ -1,5 +1,6 @@ name: "๐ŸŽซ Gists" cost: 1 GraphQL request per 100 gists +categorie: github supports: - user inputs: diff --git a/source/plugins/habits/index.mjs b/source/plugins/habits/index.mjs index 2af642e1..e91280af 100644 --- a/source/plugins/habits/index.mjs +++ b/source/plugins/habits/index.mjs @@ -100,7 +100,7 @@ console.debug(`metrics/compute/${login}/plugins > habits > running linguist`) ;(await imports.run(`${prefix} github-linguist --breakdown`, {cwd:path})) //Parse linguist result - .split("\n").map(line => line.match(/(?[\d.]+)%\s+(?\w+)/)?.groups).filter(line => line) + .split("\n").map(line => line.match(/(?[\d.]+)%\s+(?[\s\S]+)$/)?.groups).filter(line => line) .map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100) habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a) } diff --git a/source/plugins/habits/metadata.yml b/source/plugins/habits/metadata.yml index 8af6f2f4..3155c3b8 100644 --- a/source/plugins/habits/metadata.yml +++ b/source/plugins/habits/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ’ก Coding habits" cost: 1 REST request per 100 events + 1 REST request pet commit +categorie: github supports: - user - organization diff --git a/source/plugins/isocalendar/metadata.yml b/source/plugins/isocalendar/metadata.yml index 914a5996..b6bc11d1 100644 --- a/source/plugins/isocalendar/metadata.yml +++ b/source/plugins/isocalendar/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ“… Isometric commit calendar" cost: 2-3 REST requests +categorie: github supports: - user inputs: diff --git a/source/plugins/languages/metadata.yml b/source/plugins/languages/metadata.yml index a8fcb5a5..ba2552e7 100644 --- a/source/plugins/languages/metadata.yml +++ b/source/plugins/languages/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿˆท๏ธ Most used languages" cost: 0 API request +categorie: github supports: - user - organization diff --git a/source/plugins/lines/metadata.yml b/source/plugins/lines/metadata.yml index f16df2d4..2f7c851a 100644 --- a/source/plugins/lines/metadata.yml +++ b/source/plugins/lines/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ‘จโ€๐Ÿ’ป Lines of code changed" cost: 1 REST request per repository +categorie: github supports: - user - organization diff --git a/source/plugins/music/metadata.yml b/source/plugins/music/metadata.yml index 036d0f17..9fa1344d 100644 --- a/source/plugins/music/metadata.yml +++ b/source/plugins/music/metadata.yml @@ -1,5 +1,6 @@ name: "๐ŸŽผ Music plugin" cost: N/A +categorie: social supports: - user - organization diff --git a/source/plugins/nightscout/metadata.yml b/source/plugins/nightscout/metadata.yml index 8d066288..74f1d6f6 100644 --- a/source/plugins/nightscout/metadata.yml +++ b/source/plugins/nightscout/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ’‰ Nightscout" cost: N/A +categorie: health supports: - user - organization @@ -17,7 +18,7 @@ inputs: type: string default: https://example.herokuapp.com - # Controls how big the graph is + # Controls how big the graph is plugin_nightscout_datapoints: description: How many datapoints to show on the graph. 0 and 1 disable the graph. type: number diff --git a/source/plugins/pagespeed/metadata.yml b/source/plugins/pagespeed/metadata.yml index ffd3db21..008fdd43 100644 --- a/source/plugins/pagespeed/metadata.yml +++ b/source/plugins/pagespeed/metadata.yml @@ -1,5 +1,6 @@ name: "โฑ๏ธ Website performances" cost: N/A +categorie: social supports: - user - organization diff --git a/source/plugins/people/metadata.yml b/source/plugins/people/metadata.yml index eb7d5115..2d607ec6 100644 --- a/source/plugins/people/metadata.yml +++ b/source/plugins/people/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ People plugin" cost: 1 GraphQL request per 100 users + 1 REST request per user in "plugin_people_thanks" +categorie: github supports: - user - organization diff --git a/source/plugins/posts/metadata.yml b/source/plugins/posts/metadata.yml index b52b1227..a53ef737 100644 --- a/source/plugins/posts/metadata.yml +++ b/source/plugins/posts/metadata.yml @@ -1,5 +1,6 @@ name: "โœ’๏ธ Recent posts" cost: N/A +categorie: social supports: - user - organization diff --git a/source/plugins/projects/metadata.yml b/source/plugins/projects/metadata.yml index e22ee3e9..d2675d68 100644 --- a/source/plugins/projects/metadata.yml +++ b/source/plugins/projects/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ—‚๏ธ Projects" cost: 1 GraphQL request + 1 GraphQL request per repository project +categorie: github supports: - user - organization diff --git a/source/plugins/stargazers/metadata.yml b/source/plugins/stargazers/metadata.yml index 6488d416..a2f18003 100644 --- a/source/plugins/stargazers/metadata.yml +++ b/source/plugins/stargazers/metadata.yml @@ -1,5 +1,6 @@ name: "โœจ Stargazers over last weeks" cost: 1 GraphQL request per 100 stargazers +categorie: github supports: - user - organization diff --git a/source/plugins/stars/metadata.yml b/source/plugins/stars/metadata.yml index 68bf5007..2dafd3db 100644 --- a/source/plugins/stars/metadata.yml +++ b/source/plugins/stars/metadata.yml @@ -1,5 +1,6 @@ name: "๐ŸŒŸ Recently starred repositories" cost: 1 GraphQL request +categorie: github supports: - user inputs: diff --git a/source/plugins/topics/metadata.yml b/source/plugins/topics/metadata.yml index dca0e6d6..bf858eb6 100644 --- a/source/plugins/topics/metadata.yml +++ b/source/plugins/topics/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿ“Œ Starred topics" cost: N/A +categorie: github supports: - user inputs: diff --git a/source/plugins/traffic/metadata.yml b/source/plugins/traffic/metadata.yml index 0ba5cdaf..c1c889d9 100644 --- a/source/plugins/traffic/metadata.yml +++ b/source/plugins/traffic/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿงฎ Repositories traffic" cost: 1 REST request per repository +categorie: github supports: - user - organization diff --git a/source/plugins/tweets/metadata.yml b/source/plugins/tweets/metadata.yml index 8594d9cc..3e207935 100644 --- a/source/plugins/tweets/metadata.yml +++ b/source/plugins/tweets/metadata.yml @@ -1,5 +1,6 @@ name: "๐Ÿค Latest tweets" cost: N/A +categorie: social supports: - user - organization diff --git a/source/plugins/wakatime/metadata.yml b/source/plugins/wakatime/metadata.yml index c57af522..c6a09a1b 100644 --- a/source/plugins/wakatime/metadata.yml +++ b/source/plugins/wakatime/metadata.yml @@ -1,5 +1,6 @@ name: "โฐ WakaTime plugin" cost: N/A +categorie: social supports: - user inputs: diff --git a/source/templates/classic/partials/stargazers.ejs b/source/templates/classic/partials/stargazers.ejs index 3468b7ff..bec2b278 100644 --- a/source/templates/classic/partials/stargazers.ejs +++ b/source/templates/classic/partials/stargazers.ejs @@ -14,7 +14,7 @@

Total stargazers

- <% { let previous = null; for (const [date, value] of Object.entries(plugins.stargazers.total.dates)) { const p = 0.05+0.95*(value-plugins.stargazers.total.min)/(plugins.stargazers.total.max-plugins.stargazers.total.min); const [y, m, d] = date.split("-").map(Number) %> + <% { let previous = null; for (const [date, value] of Object.entries(plugins.stargazers.total.dates)) { const p = 0.05+0.95*(value-plugins.stargazers.total.min)/(plugins.stargazers.total.max-plugins.stargazers.total.min || 1); const [y, m, d] = date.split("-").map(Number) %>
<%= (value-(previous ?? 0)) ? f(value) : "" %>
@@ -29,7 +29,7 @@

New stargazers per day

- <% { let previous = null; for (const [date, value] of Object.entries(plugins.stargazers.increments.dates)) { const p = value/plugins.stargazers.increments.max; const [y, m, d] = date.split("-").map(Number) %> + <% { let previous = null; for (const [date, value] of Object.entries(plugins.stargazers.increments.dates)) { const p = value/(plugins.stargazers.increments.max || 1); const [y, m, d] = date.split("-").map(Number) %>
<%= value != 0 ? f(value, {sign:true}) : "" %>
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index db27b723..e6cf6efb 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -347,6 +347,7 @@ width: 100%; margin: 8px 0 4px; flex-grow: 1; + min-height: 70px; } .chart-bars .entry {