holy moly

pangratz prattles

GitHub Dashboard #10

Goal of this iteration: Show events for repositories

In this iteration events for a repository are added. These events seem kind of useful to me since they allow to keep up to date what’s happening in the repositories. Which issues have been created and closed, how many pushs happened, what changed in the wiki, …

Because we are going to deal with events I added a Dashboard.Event model:

app/lib/model.js
1
2
3
4
5
6
7
8
Dashboard.Event = DS.Model.extend({
  type: DS.attr('string'),
  created_at: DS.attr('string'),
  actor: function() { return this.get('data.actor'); }.property('data.actor'),
  repo: function() { return this.get('data.repo'); }.property('data.repo'),
  org: function() { return this.get('data.org'); }.property('data.org'),
  payload: function() { return this.get('data.payload'); }.property('data.payload')
});

To get the events for a repository, I changed the findQuery method on the adapter:

app/lib/github_adapter.js
1
2
3
4
5
6
7
8
9
10
11
findQuery: function(store, type, query, modelArray) {
  if (Dashboard.Repository.detect(type) && 'watched' === query.type) {
    this.watchedRepositories(query.username, modelArray, 'load');
  } else if (Dashboard.Event.detect(type) && query.username && query.repository) {
    this.repositoryEvents(query.username, query.repository, modelArray, 'load');
  }
},

repositoryEvents: function(username, repository, target, callback) {
  this.ajax('/repos/%@/%@/events'.fmt(username, repository), target, callback);
}

The controller which holds all events which shall be shown is a simple Ember.ArrayController, implemented similar to the available ones:

app/libc/controller.js
1
Dashboard.EventsController = Ember.ArrayController.extend();

Alright, everything is ready to implement the view for the events. The GitHub API returns events of different types, as stated in the documentation. Each event holds different information so it’s a good idea to implement a template for each type of event. Luckily, the API specifies the type of the event in the type property of the hash. This allows us to define a base view for all events, which then returns the name of the template which shall be used, based on the value of this very type property. So, let’s create a base view for an event:

1
2
3
4
5
6
7
8
EventView: Ember.View.extend({
  event: null,
  templateName: 'events/githubEvent',
  avatarUrl: function() {
      var gravatarId = this.get('event.actor.gravatar_id');
      return 'http://www.gravatar.com/avatar/%@'.fmt(gravatarId);
  }.property('event.actor.gravatar_id')
})

with the corresponding template:

lib/templates/events/githubEvent.handlebars (githubEvent.handlebars) download
1
2
3
4
5
<div class="profilePic" >
  <img width="30" {{bindAttr src="view.avatarUrl"}} ></img>
</div>

{{view view.DetailView eventBinding="event" }}

Inside the template, basically just the gravatar of the actor of the event is shown and another view is referenced. This DetailView is declared inside the EventView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EventView: Ember.View.extend({
  templateName: 'events/githubEvent',

  avatarUrl: function() {
      var gravatarId = this.get('event.actor.gravatar_id');
      return 'http://www.gravatar.com/avatar/%@'.fmt(gravatarId);
  }.property('event.actor.gravatar_id'),

  DetailView: Ember.View.extend({
    templateName: function() {
      var type = this.get('event.type');
      return 'events/%@-template'.fmt(type);
    }.property('event.type')
  })
})

Inside the DetailView is where the magic happens: the templateName is returned based on the value of the event.type property. So this will return events/WatchEvent-template for the WatchEvent. All templates for a specific type of event have the same structure. They look similar to this:

lib/templates/events/WatchEvent-template.handlebars (WatchEvent-template.handlebars) download
1
{{#view view.ActorView}} {{event.payload.action}} watching{{/view}}

Since all events will look something like USERNAME started watching ABC or USERNAME created tag DEF, an ActorView is created, which renders this common stuff. The ActorView is declared in the DetailView:

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
EventView: Ember.View.extend({
  templateName: 'events/githubEvent',

  avatarUrl: function() {
      var gravatarId = this.get('event.actor.gravatar_id');
      return 'http://www.gravatar.com/avatar/%@'.fmt(gravatarId);
  }.property('event.actor.gravatar_id'),

  DetailView: Ember.View.extend({
    templateName: function() {
      var type = this.get('event.type');
      return 'events/%@-template'.fmt(type);
    }.property('event.type'),

    ActorView: Ember.View.extend({
      layoutName: 'events/actor',
      defaultTemplate: Ember.Handlebars.compile(''),
      tagName: '',
      eventBinding: 'parentView.event',

      href: function() {
        var login = this.get('event.actor.login');
        return 'https://github.com/%@'.fmt(login);
      }.property('event.actor.login')
    })
  })
})

where the actor layout is defined as follows:

lib/templates/events/actor-template.handlebars (actor.handlebars) download
1
<strong><a {{action showUser context="event.actor.login"}} >{{event.actor.login}}</a> {{yield}}</strong>

The yield helper inside the actor template inserts a template: so in case of the WatchEvent this will result in something like this:

(WatchEvent-result.handlebars) download
1
2
3
<strong>
  <a {{action showUser context="event.actor.login"}} >{{event.actor.login}}</a> {{event.payload.action}} watching
</strong>

And now there is basic support to render different events with handlebars. Luckily, I have already written most of the templates for another project, so this was basically just a good old copy and paste. The templates and views have been added in commit ab996d7.

The final part is now to connect everything together inside the root.user.repository route:

app/lib/router.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
repository: Ember.Route.extend({
  route: '/:repository',
  connectOutlets: function(router, context) {
    var username = router.get('userController.username');
    var repoName = context.repository;

    // fetch repo for current user
    var repo = router.get('store').find(Dashboard.Repository, '%@/%@'.fmt(username, repoName));
    router.set('repositoryController.content', repo);
    router.get('applicationController').connectOutlet('repository');

    // get all events for this repository, and connect to the events outlet
    var events = router.get('store').findQuery(Dashboard.Event, {
      username: username,
      repository: repoName
    });
    router.get('repositoryController').connectOutlet('events', 'events', events);
  }
}),

where the template for the repository has been adapted to include an outlet for the events:

lib/templates/repsository.handlebars (repository.handlebars) download
1
2
3
4
5
6
7
8
9
10
11
12
<div class="well" >
    <a {{action showUser }} >{{owner.login}}</a> / {{name}} – <a {{bindAttr href="html_url"}}>show@GitHub</a>
    <dl class="dl-horizontal">
    {{description}}
    <dl class="dl-horizontal">
    <span class="badge" title="watchers" ><i class="icon-eye-open" ></i> {{watchers}}</span>
    <span class="badge" title="forks" ><i class="icon-heart" ></i> {{forks}}</span>
    <span class="badge" ><i class="icon-tag" ></i> {{language}}</span>
</div>
<div class="well events" >
    {{outlet "events"}}
</div>

Roundup

So, this has been a big change: show events for a repository. The hardest part was to copy the templates from the existing repository into this dashboard repository. Implementing the call to the GitHub API as well as connecting the view with the data has been easy as a breeze. Ember.js indeed is a very handsome framework.

The result of this post’s changes are available at tag v0.0.10 (changes). As always, the result is deployed at code418.com/dashboard.

Comments