Description
We open-sourced Dasherize a few days ago.
Dasherize is a simple, material-designed dashboard for your projects on which you can see:
- CI status of
master
branch (supports Travis CI, Codeship and CircleCI) - GitHub Pull Requests and Issues count and a peek of most recents
More importantly, Dasherize also has a presentation mode for big screen displays.
The README has more details of how Dasherize came about, so you can read that.
This blog post dives more into the technical details.
Turbolinks 3
Dasherize 3 uses Turbolinks 3 🎩. In fact, it's tracking master
of Turbolinks now.
Specifically, it uses the Partial Replacement ✨ technique that's only available in Turbolinks 3.
Which Feature?
Turbolinks is used to load each "Card" on the dashboard.
Code Walk Through
When the dashboard loads, it first fills the dashboard with empty "Cards" (name only) for each project.
The code can be found in app/views/projects/index.html.slim
, and the loop is:
- if @projects.present?
.row.mar-lg-top
- @projects.each do |project|
.col.s12.m4
.project id="project:#{project.id}"
= link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'
.card-panel.no-padding.grey.darken-1
.card-heading
.card-title
= link_to project.repo_name
.right
= link_to icon("gear"), edit_project_path(project), class: "gear"
.card-status.center.progress
.indeterminate
The important bit are the two lines below, while the rest are just markup that creates an empty "Card" with a progress bar.
.project id="project:#{project.id}"
= link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'
The id
is important because this is the id
to be used for Turbolinks Partial Replacement, so that a specific .project
can be swapped out with a server response.
Next, the anchor tag links to the project_path(project)
which is a RESTful path to projects#show
that shows (the "Card" for) one project.
The magic happens with remote: true
and some JavaScript. When the page loads, JavaScript will trigger a click on all the anchor tags with .js-project
class.
// app/assets/javascripts/projects.js
$(".js-project").not('.in-progress').addClass('in-progress').click();
As each link has remote: true
, each click results in an async call to projects#show
which looks like:
# app/controllers/projects_controller.rb
def show
@project = ProjectDecorator.new(@project)
@project.process_with(current_user.oauth_account.oauth_token)
render change: "project:#{@project.id}"
end
If you noticed, the last line of the method show
reads render change: "project:#{@project.id}"
.
Let's break it down:
render
with change
instructs Turbolinks 3 to render the response (instead of doing a normal page load).
change: "project:#{@project.id}"
instructs Turbolinks 3 to replace only the div
with a matching id
that can be found in the rendering app/views/projects/show.html.slim
.
And so, one by one, the empty "Cards" will be replaced by "Cards" with information.
As of this writing, Turbolinks 3's Partial Replacement technique looks really promising to me. In fact, before Turbolinks 3, I would write custom JS that sort of mimics the behavior of Partial Replacement. Hence I am really looking forward to the release of Turbolinks 5 as that means I don't need to write extra JS anymore.
There is a potential problem which I am keeping track of though:
turbolinks/turbolinks-classic#546
Parallel Tests
You are probably familiar with Parallel Tests but not so much of the gem that powers it: Parallel.
If you look into the source code, you will notice that I am actually not storing anything in the database (except for projects). Hence in order to make API calls ((GitHub + CI) * Number of Projects
) speedy, Parallel
is used to parallelize the API calls.
Back to app/controllers/projects_controller.rb
again, where we first instantiate a ProjectDecorator
, then we invoke process_with
with the user's GitHub OAuth token:
# app/controllers/projects_controller.rb
def show
@project = ProjectDecorator.new(@project)
@project.process_with(current_user.oauth_account.oauth_token)
render change: "project:#{@project.id}"
end
The implementation of process_with
is as follows:
# app/models/project_decorator
def process_with(oauth_token=nil)
@oauth_token = oauth_token
call_apis
end
The magic in this case happens in the private method call_apis
which invokes other methods:
def call_apis
Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end
def api_functions
[
method(:init_repos),
method(:init_ci)
]
end
def init_repos
client = Octokit::Client.new(access_token: @oauth_token)
@_issues = client.issues(repo_name)
end
def init_ci
@_ci =
case ci_type
when "travis"
Status::Travis.new(repo_name, travis_token).run!
when "codeship"
Status::Codeship.new(repo_name, codeship_uuid).run!
when "circleci"
Status::Circleci.new(repo_name, circleci_token).run!
else
Status::Null.new
end
end
In the method call_apis
, Parallel
was used to fork 2 threads (api_functions.size
), and to split and execute the methods in api_functions
in separate threads.
def call_apis
Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end
Using method(:init_repos)
and method(:init_ci)
, these two methods become function pointers that we can pass it as arguments to Parallel.each
and be eventually invoked with func.call
.
As such, to call both GitHub and CI apis for a project, no waiting is required to make the two api calls. With Parallel
, it helped to reduce the time required for making all API calls, and thus made the dashboard load speedily.
I had fun building Dasherize as a toy utility project.
I hope you enjoyed reading about some of the technical details too. 😊 Thanks for reading!
About Jolly Good Code
We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.