4 posts on GitHub

Releasing MaVoice: A free app to vote on repo issues

2 min read 0 comments Report broken page

First off, some news: I agreed to be this year’s CSS content lead for the Web Almanac! One of the first things to do is to flesh out what statistics we should study to answer the question “What is the state of CSS in 2020?”. You can see last year’s chapter to get an idea of what kind of statistics could help answer that question.

Of course, my first thought was “We should involve the community! People might have great ideas of statistics we could study!”. But what should we use to vote on ideas and make them rise to the top?

I wanted to use a repo to manage all this, since I like all the conveniences for managing issues. However, there is not much on Github for voting. You can add 👍 reactions, but not sort by them, and voting itself is tedious: you need to open the comment, click on the reaction, then go back to the list of issues, rinse and repeat. Ideally, I wanted something like UserVoice™️, which lets you vote with one click, and sorts proposals by votes.

And then it dawned on me: I’ll just build a Mavo app on top of the repo issues, that displays them as proposals to be voted on and sorts by 👍 reactions, UserVoice™️-style but without the UserVoice™️ price tag. 😎 In fact, I had started such a Mavo app a couple years ago, and never finished or released it. So, I just dug it up and resurrected it from its ashes! It’s — quite fittingly I think — called MaVoice.

You can set it to any repo via the repo URL parameter, and any label via the labels URL param (defaults to enhancement) to create a customized URL for any repo you want in seconds! For example, here’s the URL for the css-almanac repo, which only displays issues with the label “proposed stat”: https://projects.verou.me/mavoice/?repo=leaverou/css-almanac&labels=proposed%20stat

While this did need some custom JS, unlike other Mavo apps which need none, I’m still pretty happy I could spin up this kind of app with < 100 lines of JS :)

Yes, it’s still rough around the edges, and I’m sure you can find many things that could be improved, but it does the job for now, and PRs are always welcome 🤷🏽‍♀️

The main caveat if you decide to use this for your own repo: Because (to my knowledge) Github API still does not provide a way to sort issues by 👍 reactions, or even reactions in general (in either the v3 REST API, or the GraphQL API), issues are instead requested sorted by comment count, and are sorted by 👍 reactions client-side, right before render. Due to API limitations, this API call can only fetch the top 100 results. This means that if you have more than 100 issues to display (i.e. more than 100 open issues with the given label), it could potentially be inaccurate, especially if you have issues with many reactions and few comments.

Another caveat is that because this is basically reactions on Github issues, there is no limit on how many issues someone can vote on. In theory, if they’re a bad actor (or just overexcited), they can just vote on everything. But I suppose that’s an intrinsic problem with using reactions to vote for things, having a UI for it just reveals the existing issue, it doesn’t create it.

Hope you enjoy, and don’t forget to vote on which CSS stats we should study!


Issue closing stats for any repo

6 min read 0 comments Report broken page

tl;dr: If you just want to quickly get stats for a repo, you can find the app here. The rest of this post explains how it’s built with Mavo HTML, CSS, and 0 lines of JS. Or, if you’d prefer, you can just View Source — it’s all there!

The finished app we’re going to make, find it at https://projects.verou.me/issue-closing

One of the cool things about Mavo is how it enables one to quickly build apps that utilize the Github API. At some point I wanted to compute stats about how quickly (or rather, slowly…) Github issues are closed in the Mavo repo. And what better way to build this than a Mavo app? It was fairly easy to build a prototype for that.

Displaying a list of the last 100 closed issues and the time it took to close them

To render the last 100 closed issues in the Mavo app, I first looked up the appropriate API call in Github’s API documentation, then used it in the mv-source attribute on the Mavo root, i.e. the element with mv-app that encompasses everything in my app:

<div mv-app="issueClosing"
     mv-source="https://api.github.com/repos/mavoweb/mavo/issues?state=closed&sort=updated&per_page=100"
     mv-mode="read">
	<!-- app here -->
</div>

Then, I displayed a list of these issues with:

<div mv-multiple property="issue">
	<a class="issue-number" href="https://github.com/mavoweb/mavo/issues/[number]" title="[title]" target="_blank">#[number]</a>
	took [closed_at - created_at] ms
</div>

See the Pen Step 1 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

This would work, but the way it displays results is not very user friendly (e.g. “#542 took 149627000 ms”). We need to display the result in a more readable way.

We can use the duration() function to display a readable duration such as “1 day”:

<div mv-multiple property="issue">
	<a class="issue-number" href="https://github.com/mavoweb/mavo/issues/[number]" title="[title]" target="_blank">#[number]</a>
	took [duration(closed_at - created_at)]
</div>

See the Pen Step 2 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Displaying aggregate statistics

However, a list of issues is not very easy to process. What’s the overall picture? Does this repo close issues fast or not? Time for some statistics! We want to calculate average, median, minimum and maximum issue closing time. To calculate these statistics, we need to use the times we have displayed in the previous step.

First, we need to give our calculation a name, so we can refer to its value in expressions:

<span property="timeToClose">[duration(closed_at - created_at)]</span>

However, as it currently stands, the value of this property is text (e.g. “1 day”, “2 months” etc). We cannot compute averages and medians on text! We need the property value to be a number. We can hide the actual raw value in an attribute and use the nicely formatted value as the visible content of the element, like so (we use the content attribute here but you can use any, e.g. a data-* attribute would work just as well):

<span property="timeToClose" mv-attribute="content" content="[closed_at - created_at]">[duration(timeToClose)]</span>

Note: There is a data formatting feature in the works which would simplify this kind of thing by allowing you to separate the raw value and its presentation without having to use separate attributes for them.

We can also add a class to color it red, green, or black depending on whether the time is longer than a month, shorter than a day, or in-between respectively:

<span property="timeToClose" mv-attribute="content" content="[closed_at - created_at]" class="[if(timeToClose > month(), 'long', if (timeToClose < day(), 'short'))]">[duration(timeToClose)]</span>

Now, on to calculate our statistics! We take advantage of the fact that timeToClose outside the issue collection gives us all the times, so we can compute aggregates on them. Therefore, the stats we want to calculate are simply average(timeToClose), median(timeToClose), min(timeToclose), and max(timeToClose). We put all these in a definition list:

<dl>
	<dt>Median</dt>
	<dd>[duration(median(timeToClose))]</dd>
	<dt>Average</dt>
	<dd>[duration(average(timeToClose))]</dd>
	<dt>Slowest</dt>
	<dd>[duration(max(timeToClose))]</dd>
	<dt>Fastest</dt>
	<dd>[duration(min(timeToClose))]</dd>
</dl>

See the Pen Step 3 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Making repo a variable

Now that all the functionality of my app was in place, I realized this could be useful for more repos as well. Why not make the repo a property that can be changed? So I added an input for specifying the repo: <input property="repo" mv-default="mavoweb/mavo"> and then replaced mavoweb/mavo with [repo] everywhere else, i.e. mv-source became https://api.github.com/repos/[repo]/issues?state=closed&sort=updated&per_page=100.

Avoid reload on every keystroke

This worked, but since Mavo properties are reactive, it kept trying to reload data with every single keystroke, which was annoying and wasteful. Therefore, I needed to do a bit more work so that there is a definite action that submits the change. Enter Mavo Actions!

I created two properties: repo for the actual repo and repoInput for the input. repoInput still changes on every keystroke, but it’s repo that is actually being used in the app. I wrapped the input with a <form> and added an action on the form that does this (mv-action="set(repo, repoInput)"). I also added a submit button. Since Mavo actions on forms are triggered when the form is submitted, it doesn’t matter if I press Enter on the input, or click the Submit button, both work.

Setting the repo via a URL parameter

Eventually I also wanted to be able to set the repo from the URL, so I also added a hidden repoDefault property: <meta property="repoDefault" content="[url('repo') or 'mavoweb/mavo']">, and then changed the hardcoded mv-default="mavoweb/mavo" to mv-default="[repoDefault]" on both the repo and the repoInput properties. That way one can link to stats for a specific repo, e.g. https://projects.verou.me/issue-closing/?repo=prismjs/prism

Why a repoDefault property and not just mv-default="[url('repo') or 'mavoweb/mavo']? Just keeping things DRY and avoiding having to repeat the same expression twice.

See the Pen Step 5 - Issue Closing App Tutorial by Lea Verou (@leaverou) on CodePen.

Filtering by label

At some point I wondered: What would the issue closing times be if we only counted bugs? What if we only counted enhancements? Surely these would be different: When looking at issue closing times for a repo, one primarily cares about how fast bugs are fixed, not how quickly every random feature suggestion is implemented. Wouldn’t it be cool to also have a label filter?

For that, I added a series of radio buttons:

Show:
<label><input type="radio" property="labels" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>

Then, I modified mv-source to also use this value in its API call: mv-source="https://api.github.com/repos/[repo]/issues?state=closed&sort=updated&labels=[labels]&per_page=100".

Note that when turning radio buttons into a Mavo property you only use the property attribute on the first one. This is important because Mavo has special handling when you use the property attribute with the same name multiple times in the same group, which we don’t want here. You can add the property attribute on any of the radio buttons, it doesn’t have to be the first. Just make sure it’s only one of them.

Then I became greedy: Why not also allow filtering by custom labels too? So I added another radio with an input:

Show:
<label><input type="radio" property="labels" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>
<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel"></label>

Note that since this is a text field, when the last value is selected, we’d have the same problem as we did with the repo input: Every keystroke would fire a new request. We can solve this in the same way as we solved it for the repo property, by having an intermediate property and only setting labels when the form is actually submitted:

Show:
<label><input type="radio" property="labelFilter" name="labels" checked value=""> All</label>
<label><input type="radio" name="labels" value="bug"> Bugs only</label>
<label><input type="radio" name="labels" value="enhancement"> Enhancements only</label>
<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel"></label>
<meta property="labels" content="">

Adding label autocomplete

Since we now allow filtering by a custom label, wouldn’t it be cool to allow autocomplete too? HTML allows us to offer autocomplete in our forms via <datalist> and we can use Mavo to populate the contents!

First, we add a <datalist> and link it with our custom label input, like so:

<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel" list="label-suggestions"></label>
<datalist id="label-suggestions">
</datalist>

Currently, our suggestion list is empty. How do we populate it with the labels that have actually been used in this repo? Looking at the API documentation, we see that each returned issue has a labels field with its labels as an object, and each of these objects has a name field with the textual label. This means that if we use issue.labels.name in Mavo outside of the issues collection, we get a list with all of these values, which we can then use to populate our <datalist> by passing it on to mv-value which allows us to create dynamic collections:

<label><input type="radio" name="labels" value="[customLabel]"> Label <input property="customLabel" list="label-suggestions"></label>
<datalist id="label-suggestions">
	<option mv-multiple mv-value="unique(issue.labels.name)"></option>
</datalist>

Note that we also used unique() to eliminate duplicates, since otherwise each label would appear as many times as it is used.

See the Pen Issue Closing App - Tutorial Step 6 by Lea Verou (@leaverou) on CodePen.

Adding a visual summary graphic

Now that we got the functionality down, we can be a little playful and add some visual flourish. How about a bar chart that summarizes the proportion of long vs short vs normal closing times? We start by setting the CSS variables we are going to need for our graphic, i.e. the number of issues in each category:

<summary style="--short: [count(timeToClose < day())]; --long: [count(timeToClose > month())]; --total: [count(issue)];">
	Based on [count(issue)] most recently updated issues
</summary>

Then, we draw our graphic:

summary::before {
	content: "";
	position: fixed;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: 1;
	height: 5px;
	background: linear-gradient(to right, var(--short-color) calc(var(--short, 0) / var(--total) * 100%), hsl(220, 10%, 75%) 0, hsl(220, 10%, 75%) calc(100% - var(--long, 0) / var(--total) * 100%), var(--long-color) 0) bottom / auto 100% no-repeat border-box;
}

Now, wouldn’t it be cool to also show a small pie chart next to the heading, if conic gradients are supported so we can draw it? The color stops would be the same, so we define a --summary-stops variable on summary, so we can reuse them across both gradients:

summary {
	--summary-stops: var(--short-color) calc(var(--short, 0) / var(--total) * 100%), hsl(220, 10%, 75%) 0, hsl(220, 10%, 75%) calc(100% - var(--long, 0) / var(--total) * 100%), var(--long-color) 0;
}

summary::before { content: ""; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1; height: 5px; background: linear-gradient(to right, var(–summary-stops)) bottom / auto 100% no-repeat border-box; }

@supports (background: conic-gradient(red, red)) { summary::after { content: ""; display: inline-block; vertical-align: middle; width: 1.2em; height: 1.2em; margin-left: .3em; border-radius: 50%; background: conic-gradient(var(–summary-stops)); } }

See the Pen Issue Closing App - Tutorial Step 7 by Lea Verou (@leaverou) on CodePen.


URL rewriting with Github Pages

2 min read 0 comments Report broken page

redirectI adore Github Pages. I use them for everything I can, and try to avoid server-side code like the plague, exactly so that I can use them. The convenience of pushing to a repo and having the changes immediately reflected on the website with no commit hooks or any additional setup, is awesome. The free price tag is even more awesome. So, when the time came to publish my book, naturally, I wanted the companion website to be on Github Pages.

There was only one small problem: I wanted nice URLs, like http://play.csssecrets.io/pie-animated, which would redirect to demos on dabblet.com. Any sane person would have likely bitten the bullet and used some kind of server-side language. However, I’m not a particularly sane person :D

Turns out Github uses some URL rewriting of its own on Github Pages: If you provide a 404.html, any URL that doesn’t exist will be handled by that. Wait a second, is that basically how we do nice URLs on the server anyway? We can do the same in Github Pages, by just running JS inside 404.html!

So, I created a JSON file with all demo ids and their dabblet URLs, a 404.html that shows either a redirection or an error (JS decides which one) and a tiny bit of Vanilla JS that reads the current URL, fetches the JSON file, and redirects to the right dabblet. Here it is, without the helpers:

(function(){

document.body.className = ‘redirecting’;

var slug = location.pathname.slice(1);

xhr({ src: ‘secrets.json’, onsuccess: function () { var slugs = JSON.parse(this.responseText);

var hash = slugs[slug];

if (hash) { // Redirect var url = hash.indexOf(‘http’) == 0? hash : ‘http://dabblet.com/gist/’ + hash; $(‘section.redirecting > p’).innerHTML = ‘Redirecting to <a href="’ + url + ‘">’ + url + ‘</a>…’; location.href = url; } else { document.body.className = ‘error not-found’; } }, onerror: function () { document.body.className = ‘error json’; } });

})();

That’s all! You can imagine using the same trick to redirect to other HTML pages in the same Github Pages site, have proper URLs for a single page site, and all sorts of things! Is it a hack? Of course. But when did that ever stop us? :D


Easily keep gh-pages in sync with master

1 min read 0 comments Report broken page

I always loved Github’s ability to publish pages for a project and get the strain out of your server. However, every time I tried it, I struggled to keep the gh-pages branch up to date. Until I discovered the awesome git rebase.

Usually my github workflow is like this:

git add . git status // to see what changes are going to be commited git commit -m ‘Some descriptive commit message’ git push origin master

Now, when I use gh-pages, there are only a few more commands that I have to use after the above:

git checkout gh-pages // go to the gh-pages branch git rebase master // bring gh-pages up to date with master git push origin gh-pages // commit the changes git checkout master // return to the master branch

I know this is old news to some of you (I’m a github n00b, struggling with basic stuff, so my advice is probably for other n00bs), but if I had read this a few months ago, it would’ve saved me big hassles, so I’m writing it for the others out there that are like me a few months ago.

Now if only I find an easy way to automate this… :)