Follow Zube on Twitter

Life just got a little better

Pull requests are no longer part of the main board. Instead, pull requests now appear in a list on the right side of the board. The pull request list shows all open and closed pull requests for whatever milestone you’re looking at. At the top right of the page there is a pull request icon with a red badge to let you know the number of open pull requests (so you can drop everything else going on in your life and immediately merge your coworker’s changes). Clicking the icon will show/hide the pull request list.

New pull request list

Another change that may make your day is that you can now change how priority is represented on a Zube card. Previously, priority was labeled “Code” (Code Red, Code Orange, etc.). Now, the default naming convention is P1, P2, P3, P4, and P5. Not to worry, if you love having only colors then you can change it back on the board’s settings page. You can also choose an urgency scale with the names Blocker, Critical, Major, Minor, and Trivial, if that’s your thing.

Priority names are now configurable

If you’re often looking at your Zube board, wishing you could collapse a user row because that team member just really isn’t that important, then you’re in luck! You can now collapse and expand user rows by clicking on the collapse/expand icons next to their avatar.

Collapsible user rows

There’s also one last small thing that makes life better for everyone using points (story points). The point totals are now displayed at the top of every board column. Having point totals makes it easy to see if one stage of your workflow is getting overloaded. When trying to maximize for workflow efficiency, eliminating bottlenecks is one of the most important things you can do. If you’re currently not using points (story points) and would like to enable them, you can do so on the board’s setting page.

Point totals for every column

Manage your GitHub issues like a boss

The Zube board is great for organizing and prioritizing your issues, but sometimes you just need more power! The Issue Manager is the easiest and fastest way to manage your GitHub issues… in the world.*

Issue Manager showing issues that are filtered and ordered

The Issue Manager is similar in many ways to what you get on GitHub. You can filter and sort a list of your issues by many things like state and assignee. However, the Issues Manager also has access to your Zube data so you can filter and sort by category, color code, points, and upvotes as well.

There’s also a real-time search bar that allows you to search for cards by title or by issue number.

The speed at which you can find exactly what you want is just blistering. Once you’ve found the issues you want, you can update them all in bulk. There are two ways to do this - either you can update the attributes of the selected cards, or you can bulk move the selected cards to any category you like. The power is intoxicating!

Move many issues at the same time

As a quick example, let’s say you wanted to move all of your recently commented on bugs from the backlog to a milestone To Do column. Easy peasy.

  1. Filter by backlog bugs and order by “Last comment at”
  2. Select the issues at the top of the list
  3. Click the “Move Selected Cards” button and choose the category

Done and done.

* We’ve been watching a lot of Top Gear recently here at Zube.

Tickets that seamlessly work with GitHub Issues

If you’re reading this, you’re probably as excited as we are about a ticketing system that’s fully integrated with GitHub issues in real-time. The other cool thing about the new ticketing system is that it is dynamically linked to your Zube scrum/kanban board. So no matter where you make changes, everything remains up-to-date without you or your development team having to do anything special.

We created Zube to solve specific challenges we faced as developers at growing startups. One of the main challenges we faced was the “black hole” that was the development team which, to be fair, we were a part of. While we tried our best, it takes effort to keep everyone in the loop and we always prioritized cranking out code over team communication (whether that was the right thing to do is a different story).

We created Zube to eliminate the effort needed to keep everyone on the same page. We made a scrum/kanban board that’s real-time synced with GitHub issues so that the entire development team can be on the same page, even if some team members just continue closing issues from the command line. And today, we’re super excited to unveil a ticketing system that brings that same level of visibility to the entire team, whether you’re a project manager, working on the support team, or a backend engineer.

Zube tickets, like tickets in a normal ticketing system, can be given priorities, assigned to team members, commented on, and be marked as closed when they’re complete. The cool thing about Zube tickets is their ability to be linked with GitHub issues. The cards on the lower right of the image below are GitHub issues.

A Zube ticket with three linked GitHub issues

If you change something on any of those cards, the corresponding GitHub issue will be updated in real-time, and vice versa. This is super powerful because once you’ve linked some GitHub issues to your ticket, your ticket will automatically change its status based on the linked GitHub issues. For example, once all of the linked GitHub issues are closed, your ticket will automatically change its status to “Completed”. The same goes for all stages, actually. The ticket status will automatically change from “New” to “Queued” once some of the linked cards have been triaged and from “Queued” to “In Progress” once a developer starts working on one of the linked issues. All without requiring the developer to do anything out of the ordinary.

It’s also easy get an overview of the project’s tickets (or just the tickets assigned to you) from the ticket index view.

An overview of your active tickets

We need upvotes

We think it is ridiculous that you can’t upvote GitHub issues. There are many, many open source projects that use GitHub issues for bug tracking and feature requests and there has to be a way to let the core team know that a certain bug is screwing up your whole world. So why no upvotes?

There is a hack, of course, and that’s to make a comment that says +1 or . But that sucks because +1 comments add a bunch of noise to the discussion. The best solution would be for GitHub engineers to add an upvote button the the native UI, but since that hasn’t happened yet, we’ve done our best to make things more manageable. In essence, what we’ve made is a bot that counts all comment upvotes in the background. The way it works is that it monitors your GitHub issue comments in real time for any comments that start with +1 or and keeps a unique total in the issue body like this:

Upvote count in issue body

Since upvote counting runs in the background, you don’t have to use the Zube UI for it to work. Once upvotes are enabled you’re good to go.

Having an upvote total displayed in the card body makes it easier to see at a glance which issues have the most interest. If you’re a maintainer of an open source project, you’re now free to remove the +1 comments from the comment thread. Even when +1 comments are removed from the thread, an accurate unique total is still maintained. A list of the users who upvoted any issue is available on Zube, and if there’s sufficient interest in making the list appear on the GitHub issues itself (like at the end of the card body), we’d be happy to make that happen.

Upvotes on Zube

And for those people already using Zube to manage their GitHub issues, there’s a board setting you can enable to add upvotes to every Zube card, so upvote away!

We hope you find upvote counting useful as stopgap until GitHub realizes that there’s a true need for upvotes.

When we launched Zube a month ago, there was a lot of interest around how we made the animated demo video on the homepage https://zube.io. To be honest, it was a real pain in the ass, so we thought we’d share exactly how we did it.

tl;dr:

  • Capture a movie using Quicktime or equivalent
  • ffmpeg to create a color palette from the movie
  • ffmpeg to create a gif from the movie using the color palette
  • convert to create a sequence of 8 bit png frames from the gif
  • display the pngs sequentially in the <canvas> with a Javascript loop

Requirements

There were a few requirements that we wanted to hit. First off, the video had to autoplay on page load. This was a problem on mobile browsers since they don’t autoplay videos due to bandwidth concerns. There are some hacks you can do to get around this but none of them work very well. So using an mp4 was out.

Our second requirement was that the video should not overload the browser. This meant a gif was out because, for whatever reason, large gifs crash my cofounder’s phone.

Finally, we wanted the text to be readable while keeping the total file size relatively small. This meant a series of .jpgs was out because jpg text is all blurry.

In order to hit all these requirements we decided to use Javascript to loop over a sequence of pngs. We chose this approach because the text in a png is readable and since pngs support transparency, we could overlay them to produce the animation. Since most of the frames are largely transparent, each frame is rather small in size and the total size is about the same as a gif.

One last note. Gif’s have 8-bit color to reduce file size. This gave us the idea to use 8-bit color for our pngs as well. The only problem is that 256 colors is rather limited so the colors will come out all wrong. To get around the problem, you need to generate a custom color palette, which is explained in this great article by Clément Bœsch.

The Full Procedure

Capture your demo movie using video capture software.

We used Quicktime, File -> New Screen Recording, but you do you. Quicktime generates a movie which we called demo.mov. If you program generates something else like an .mp4, that’s ok too.

Create a color palette from your movie using ffmpeg
1
ffmpeg -i demo.mov -vf fps=10,scale=1378:-1:flags=lanczos,palettegen palette.png

Put in the appropriate filename and format for the movie you generated in step 1 instead of demo.mov if you called your movie something else. The scale parameter should be set to the width of your video. In our case, demo.mov was recorded at a width of 1378px, so we used scale=1378. The resulting color palette looks pretty cool:

8-bit color palette for our movie

Next we’re going to create a gif from our movie using the color palette we generated in step 2. As a hack, we first convert the movie to a gif, and then convert the gif to a series of pngs. Our objective is to create a series of pngs, and there may be some way to use ffmpeg to directly create a series of transparent 8-bit pngs layers, but we don’t know how to do that. Just think of this step as a free gif. :)

Create a gif from the movie
1
ffmpeg -i demo.mov -i palette.png -filter_complex "fps=10,scale=1378:-1:flags=lanczos[x];[x][1:v]paletteuse" demo.gif

demo.mov is the movie from step 1, palette.png is the color palette we generated in step 2, we specifed the frame rate to be 10fps, the scale parameter of 1378 is the width of our video, and we named the resulting gif demo.gif.

Create the series of transparent 8-bit pngs using ImageMagick
1
convert -dispose Background -coalesce demo.gif -colors 256 PNG8:demo_%d.png

The resulting pngs are just the pixels that need to be added to screen in order for the animation to move forward. The first png is the initial background:

First frame of the png set

Each subsequent image is mostly transparent with some random looking opaque pixels. Here’s the next image in the animation:

Even though each frame looks weird, when we layer them on top of each other, the resulting animation will look perfectly normal.

As a note, our first instinct was to make a single large image that contained every frame a.k.a a sprite sheet. Sprite sheets in general are awesome because they reduce the number of image requests to the server, which have http overhead and are subject to browser parallel request limitations. However, when we made a sprite sheet and then tried to render it in the canvas using ctx.drawImage(), the whole browser came to a grinding halt. More specifically, Chrome came to a grinding halt. Safari, was super fast. Apparently Chrome has a bug. From our brief research we are unclear if the problem lies in trying to use drawImage() to draw subsections of a larger image, or if the problem is in how Chrome handles canvas memory. The sprite sheet we ran the test on was rather large 1,378 px × 157,762 px in size and 3.6 MB. Your mileage may vary.

The HTML

Somewhere on your page put

1
2
3
4
<div id="demo">
<img id="imac" src="images/imac.png">
<canvas id="demo-animation"></canvas>
</div>

The sibling to our canvas element is an image of an iMac. We want the demo animation to look like it is playing inside the iMac so we place both of those elements inside a parent div so there is a common point of reference.

The CSS

To make sure the canvas scales along with with parent we added the css

1
2
3
4
5
6
7
8
9
10
11
12
#demo {
position: relative;
}
#imac {
width: 100%;
}
#demo-animation {
position: absolute;
width: 91.56%;
top: 4.8%;
left: 4.19%;
}

All the magic numbers in the CSS are there to position our animation in the center of the iMac’s screen.

The Javascript

We used JS to dynamically load and display our sequence of pngs by drawing on the canvas. In our case, when we used ImageMagick to generate our images, we ended up with 202 of them ranging from demo_0.png to demo_201.png. The first thing that happens in the JS is to fetch and load the 0th image, demo_0.png. This image will act as a placeholder while the other images are loading.

Then we loop over all the possible image numbers and fetch all the images. We keep count of how many have loaded and when they have all been fetched we kick off the animation by calling renderFrame(). The renderFrame function draws an image on the screen, waits 100ms, and then calls itself. That way the frame rate is approximately 10 frames/second which is what we specified when we created the gif from the original movie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script>
var canvas = document.getElementById('demo-animation');
var ctx = canvas.getContext('2d');
var images = {};
var total_frames = 202;
var loaded_images = 0;
var current_frame = 0;
canvas.width = 1378;
canvas.height = 781;
// The animation loop
function renderFrame() {
ctx.drawImage(images[current_frame], 0, 0);
current_frame += 1;
if (current_frame === total_frames) current_frame = 0;
window.setTimeout(renderFrame, 100);
};
// Load the first image as a placeholder
images[0] = new Image();
images[0].onload = function () {
loaded_images += 1;
ctx.drawImage(images[0], 0, 0);
};
images[0].src = 'images/static/demo/demo_0.png';
// When all the remaining images are loaded, kick off the animation loop
for (var i = 1; i < total_frames; i++) {
images[i] = new Image();
images[i].onload = function () {
loaded_images += 1;
if (loaded_images === total_frames) renderFrame();
};
images[i].src = 'images/static/demo/demo_' + i + '.png';
}
</script>

Two notes on bandwidth and performance.

In production you should serve your images with a cache header so the browser only has to download your set of images one time. This means that if you ever change your set of images, you need to choose a new filename for every image or the old images may still show up. The common technique for choosing unique images is to md5 hash the image and append that string to the file name. If you decide to go the md5 hash route you’ll probably need to create a dictionary of the filenames so you can programmatically iterate over them.

I should also point out that, since the video you make autoplays on mobile, you need to be considerate of bandwidth limitations. The aggregate size of the png sequence is significantly larger than an mp4. On our site we load a smaller version of the video on browsers that are less than 768px wide. The smaller version of the video is 3x smaller than the full size video. We also kept our video short to reduce file size. As a rule of thumb, if you find the aggregate size of your video is nearing 10 MB, then you should seriously consider an mp4 instead.