diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/show.html
index 46003bd8b0f80cf99520cbec1bd37701c2cc9b01..e5f4708a6a06468058c3116f9cf00228422ff6ec 100644
--- a/vipra-ui/app/html/articles/show.html
+++ b/vipra-ui/app/html/articles/show.html
@@ -1,4 +1,6 @@
-<h1 ng-bind="::article.title"></h1>
+<div class="page-header">
+  <h1 ng-bind="::article.title"></h1>
+</div>
 
 <h3>Info <hide-link target="#info"/></h3>
 
@@ -41,15 +43,28 @@
   </div>
 </div>
 
-<h3>Topics <hide-link target="#topics"/></h3>
+<h3>Topics <span ng-bind-template="({{article.topics.length}})"></span> <hide-link target="#topics"/></h3>
 
 <div class="row" id="topics">
-  <div class="col-md-4 topic-links text-right">
-    <topic-link topic="topic.topic" ng-repeat="topic in article.topics">
-      <span class="label label-default" ng-bind-template="{{topic.share}}%"></span>
-    </topic-link>
+  <div class="col-md-5 topic-links">
+    <table class="table table-morecondensed">
+      <tr>
+        <th sort-by="topic.topic.name" sort-type="topicSort" sort-reverse="topicSortRev">
+          Name
+        </th>
+        <th sort-by="topic.share" sort-type="topicSort" sort-reverse="topicSortRev">
+          Share
+        </th>
+      </tr>
+      <tr ng-repeat="topic in article.topics | orderBy:topicSort:topicSortRev">
+        <td>
+          <topic-link topic="topic.topic"/>
+        </td>
+        <td class="text-right" ng-bind-template="{{topic.share}}%"></td>
+      </tr>
+    </table>
   </div>
-  <div class="col-md-offset-1 col-md-6">
+  <div class="col-md-7">
     <div class="pie-chart" id="topic-share" highcharts="topicShare"></div>
   </div>
 </div>
diff --git a/vipra-ui/app/html/directives/dropdown.html b/vipra-ui/app/html/directives/dropdown.html
new file mode 100644
index 0000000000000000000000000000000000000000..1da0a98057040bb994b498560bc7b3b177e9f2e2
--- /dev/null
+++ b/vipra-ui/app/html/directives/dropdown.html
@@ -0,0 +1,7 @@
+<div class="dropdown">
+  <button class="btn btn-default dropdown-toggle" type="button" ng-attr-id="{{dropdownId}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+    <span ng-bind="label"></span>
+    <span class="caret"></span>
+  </button>
+  <ul ng-attr-class="{{'dropdown-menu ' + align}}" ng-attr-aria-labelledby="{{dropdownId}}" ng-transclude></ul>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/network.html b/vipra-ui/app/html/network.html
index 4346bb6018cdf12a46c5ced38da41badb31d937f..5aa49fbe3e115f764e263a51446bc3ee9ec0e822 100644
--- a/vipra-ui/app/html/network.html
+++ b/vipra-ui/app/html/network.html
@@ -1,7 +1,14 @@
-<div class="fullsize navpadding" vis-network vis-type="type" vis-colors="colors" vis-shown="shown" ng-model="model">
+<div class="fullsize navpadding">
     <div class="graph-legend overlay">
-      <span style="color:{{colors.articles}}"><input type="checkbox" ng-model="shown.articles"> Articles</span><br>
-      <span style="color:{{colors.topics}}"><input type="checkbox" ng-model="shown.topics"> Topics</span><br>
-      <span style="color:{{colors.words}}"><input type="checkbox" ng-model="shown.words"> Words</span><br>
+      <label style="color:{{colors.articles}}">
+        <input type="checkbox" ng-model="shown.articles" store-value="showArticles"> Articles
+      </label>
+      <label style="color:{{colors.topics}}">
+        <input type="checkbox" ng-model="shown.topics" store-value="showTopics"> Topics
+      </label>
+      <label style="color:{{colors.words}}">
+        <input type="checkbox" ng-model="shown.words" store-value="showWords"> Words
+      </label>
     </div>
+    <div class="fullsize navpadding" id="visgraph"></div>
 </div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index 3f11638f931fdedfb5caa7f57bd70abf7297504c..753ba0cb6fb51391e2ce0ab4f73a108cddab9836 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -1,45 +1,78 @@
-<h1 ng-bind="::topic.name"></h1>
+<div class="page-header">
+  <h1>
+    <div ng-bind="topic.name" ng-hide="isRename"></div>
+    <div class="input-group input-group-lg" ng-show="isRename">
+      <input type="text" class="form-control" ng-model="topic.name" id="topicName" ng-keyup="keyup($event)">
+      <div class="input-group-btn">
+        <button class="btn btn-success" ng-click="endRename(true)">
+          <span class="glyphicon glyphicon-ok"></span>
+        </button>
+        <button class="btn btn-danger" ng-click="endRename(false)">
+          <span class="glyphicon glyphicon-remove"></span>
+        </button>
+      </div>
+    </div>
+  </h1>
+  
+  <bs-dropdown label="Actions">
+    <li><a ng-click="startRename()">Rename</a></li>
+  </bs-dropdown>
+</div>
 
-<table class="table table-bordered table-condensed">
-  <tbody>
-    <tr>
-      <th>ID</th>
-      <td ng-bind="::topic.id"></td>
-    </tr>
-    <tr>
-      <th>Index</th>
-      <td ng-bind="::topic.index"></td>
-    </tr>
-    <tr>
-      <th>Created</th>
-      <td ng-bind="::topic.created"></td>
-    </tr>
-    <tr>
-      <th>Last modified</th>
-      <td ng-bind="::topic.modified"></td>
-    </tr>
-    <tr>
-      <th>Links</th>
-      <td>
-        <a ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
-      </td>
-    </tr>
-  </tbody>
-</table>
+<h3>Info <hide-link target="#info"/></h3>
+
+<div class="row" id="info">
+  <div class="col-md-12">
+    <table class="table table-bordered table-condensed">
+      <tbody>
+        <tr>
+          <th>ID</th>
+          <td ng-bind="::topic.id"></td>
+        </tr>
+        <tr>
+          <th>Index</th>
+          <td ng-bind="::topic.index"></td>
+        </tr>
+        <tr>
+          <th>Created</th>
+          <td ng-bind="::topic.created"></td>
+        </tr>
+        <tr>
+          <th>Last modified</th>
+          <td ng-bind="::topic.modified"></td>
+        </tr>
+        <tr>
+          <th>Links</th>
+          <td>
+            <a ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
 
 <h3>Words <hide-link target="#words"/></h3>
 
-<table class="table table-bordered table-condensed" id="words">
-  <thead>
-    <tr>
-      <th>Word</th>
-      <th>Likeliness</th>
-    </tr>
-  </thead>
-  <tbody>
-    <tr ng-repeat="word in ::topic.words">
-      <td><a ui-sref="words.show({id:word.id})" ng-bind="word.id"></a></td>
-      <td ng-bind="word.likeliness"></td>
-    </tr>
-  </tbody>
-</table>
\ No newline at end of file
+<div class="row" id="words">
+  <div class="col-md-12">
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th sort-by="id" sort-type="wordSort" sort-reverse="wordSortRev">
+            Word
+          </th>
+          <th sort-by="likeliness" sort-type="wordSort" sort-reverse="wordSortRev">
+            Likeliness
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="word in topic.words | orderBy:wordSort:wordSortRev">
+          <td><a ui-sref="words.show({id:word.id})" ng-bind="word.id"></a></td>
+          <td ng-bind="word.likeliness"></td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/words/show.html b/vipra-ui/app/html/words/show.html
index 1ab0373b9d80526fe1895bf5c55481f6a77c62a6..749c267fd716f73df8c5baf240b26c1cc5db0a79 100644
--- a/vipra-ui/app/html/words/show.html
+++ b/vipra-ui/app/html/words/show.html
@@ -1,18 +1,30 @@
-<h1 ng-bind="::word.id"></h1>
+<div class="page-header">
+  <h1 ng-bind="::word.id"></h1>
+</div>
 
-<table class="table table-bordered table-condensed">
-  <tbody>
-    <tr>
-      <th>Created</th>
-      <td ng-bind="::word.created"></td>
-    </tr>
-  </tbody>
-</table>
+<h3>Info <hide-link target="#info"/></h3>
 
-<h3>Topics</h3>
+<div class="row" id="info">
+  <div class="col-md-12">
+    <table class="table table-bordered table-condensed">
+      <tbody>
+        <tr>
+          <th>Created</th>
+          <td ng-bind="::word.created"></td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>
 
-<ul class="list-unstyled">
-  <li ng-repeat="topic in ::word.topics">
-    <topic-link topic="topic"/>
-  </li>
-</ul>
\ No newline at end of file
+<h3>Topics <hide-link target="#topics"/></h3>
+
+<div class="row" id="topics">
+  <div class="col-md-12">
+    <ul class="list-unstyled">
+      <li ng-repeat="topic in ::word.topics">
+        <topic-link topic="topic"/>
+      </li>
+    </ul>
+  </div>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js
index 6b6456a299d79b789abb2e9bf0a08a4144c29620..be65d8ccde29f2e537f8b56e32c985c8970e7565 100644
--- a/vipra-ui/app/js/app.js
+++ b/vipra-ui/app/js/app.js
@@ -8,12 +8,10 @@
     'ngResource',
     'ngSanitize',
     'ui.router',
-    'ui.bootstrap',
     'vipra.controllers',
     'vipra.directives',
     'vipra.factories',
     'vipra.filters',
-    'vipra.services',
     'vipra.templates'
   ]);
 
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index ff261ec5c5a061bb6012ad866c40b52ed3e5b187..f8a5cd6b21256f0b91efcf5d6f3ace84c1d80374 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -14,6 +14,9 @@
       pageSize = 100,
       paginationPadding = 4;
 
+  /**
+   * Index controller
+   */
   app.controller('IndexController', ['$scope', '$location', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'SearchFactory',
     function($scope, $location, ArticleFactory, TopicFactory, WordFactory, SearchFactory) {
 
@@ -48,19 +51,46 @@
 
   }]);
 
-  app.controller('NetworkController', ['$scope', '$state', '$stateParams', 'ArticleFactory', 'TopicFactory',
-    function($scope, $state, $stateParams, ArticleFactory, TopicFactory) {
+  /**
+   * Network controller
+   */
+  app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'Store', 'ArticleFactory', 'TopicFactory', 'WordFactory',
+    function($scope, $state, $stateParams, $timeout, Store, ArticleFactory, TopicFactory, WordFactory) {
+
+    var id = 0,
+        ids = {},
+        container = $("#visgraph")[0],
+        edges = {};
 
-    $scope.type = $stateParams.type;
     $scope.colors = {
       articles: '#BBC9D2',
       topics: '#DBB234',
       words: '#547C65'
     };
-    $scope.shown = {
-      articles: true,
-      topics: true,
-      words: true
+    $scope.nodes = new vis.DataSet();
+    $scope.edges = new vis.DataSet();
+    $scope.data = {
+      nodes: $scope.nodes,
+      edges: $scope.edges
+    };
+    $scope.options = {
+      nodes: {
+        font: { size: 14 },
+        shape: 'dot',
+        borderWidth: 0
+      },
+      edges: {
+        color: {
+          highlight: '#f00'
+        }
+      },
+      layout: { randomSeed: 1 },
+      physics: {
+        barnesHut: {
+          springConstant: 0.008,
+          gravitationalConstant: -3500
+        }
+      }
     };
 
     var factory;
@@ -68,21 +98,155 @@
       factory = ArticleFactory;
     else if($stateParams.type === 'topics')
       factory = TopicFactory;
+    else if($stateParams.type === 'words')
+      factory = WordFactory;
     else {
       console.log('unknown network type');
       return;
     }
 
+    // get root node
     factory.get({id: $stateParams.id}, function(response) {
-      $scope.model = response.data;
+      // add root node
+      if($stateParams.type === 'articles')
+        $scope.nodes.add([articleNode(response.data)]);
+      else if($stateParams.type === 'topics')
+        $scope.nodes.add([topicNode(response.data)]);
+      else if($stateParams.type === 'words')
+        $scope.nodes.add([wordNode(response.data)]);
+      ids[response.data.id] = id;
+
+      // create graph
+      $scope.graph = new vis.Network(container, $scope.data, $scope.options);
+      $scope.graph.on('selectNode', $scope.select);
+      $scope.graph.on('doubleClick', $scope.open);
     });
 
+    var newNode = function(title, type, show, dbid, color, shape) {
+      ids[dbid] = ++id;
+      return {
+        id: id,
+        title: title,
+        label: title.multiline(5),
+        type: type,
+        show: show,
+        dbid: dbid,
+        shape: shape || 'dot',
+        color: {
+          background: color,
+          highlight: { background: color }
+        }
+      };
+    };
+
+    var topicNode = function(topic) {
+      topic = topic.topic || topic;
+      return newNode(topic.name, 'topic', 'topics.show', topic.id, $scope.colors.topics, 'triangle');
+    };
+
+    var articleNode = function(article) {
+      return newNode(article.title, 'article', 'articles.show', article.id, $scope.colors.articles, 'square');
+    };
+
+    var wordNode = function(word) {
+      return newNode(word.id, 'word', 'words.show', word.id, $scope.colors.words);
+    };
+
+    var edgeExists = function(idA, idB) {
+      if(idB < idA) {
+        var tmp = idA;
+        idA = idB;
+        idB = tmp;
+      }
+      return edges.hasOwnProperty(idA + '-' + idB);
+    };
+
+    var addEdge = function(idA, idB) {
+      if(idB < idA) {
+        var tmp = idA;
+        idA = idB;
+        idB = tmp;
+      }
+      edges[idA + '-' + idB] = 1;
+    };
+
+    // construct new nodes
+    var constructor = function(result, node, key, nodeFunction) {
+      if(result.data && (!key || result.data[key])) {
+        var data = key ? result.data[key] : result.data,
+            newNodes = [],
+            newEdges = [];
+        for(var i = 0; i < data.length; i++) {
+          var current = data[i];
+          if(ids.hasOwnProperty(current.id)) {
+            if(edgeExists(ids[current.id], node.id))
+              continue;
+            newEdges.push({from:ids[current.id], to:node.id});
+            addEdge(ids[current.id], node.id);
+          } else {
+            newNodes.push(nodeFunction(current));
+            newEdges.push({from:id, to:node.id});
+            addEdge(id, node.id);
+          }
+        }
+        if(newNodes.length)
+          $scope.nodes.add(newNodes);
+        if(newEdges.length)
+          $scope.edges.add(newEdges);
+      }
+    };
+    
+    // on node select
+    var selectTimeout;
+    $scope.select = function(props) {
+      $timeout.cancel(selectTimeout);
+      selectTimeout = $timeout(function() {
+        var node = $scope.nodes.get(props.nodes[0]);
+        if(node) {
+          if(node.type === 'article' && $scope.shown.topics) {
+            // node is article, load article to get topics
+            ArticleFactory.get({id:node.dbid}, function(res) {
+              for(var i = 0; i < res.data.topics.length; i++)
+                res.data.topics[i] = res.data.topics[i].topic;
+              constructor(res, node, 'topics', topicNode);
+            });
+          } else if(node.type === 'topic') {
+            // node is topic, load topic to get words and articles
+            if($scope.shown.words)
+              TopicFactory.get({id:node.dbid}, function(res) {
+                constructor(res, node, 'words', wordNode);
+              });
+            if($scope.shown.articles)
+              TopicFactory.articles({id:node.dbid}, function(res) {
+                constructor(res, node, null, articleNode);
+              });
+          } else if(node.type === 'word' && $scope.shown.topics) {
+            // node is word, load word to get topics
+            WordFactory.get({id:node.dbid}, function(res) {
+              constructor(res, node, 'topics', topicNode);
+            });
+          }
+          $scope.nodes.update(node);
+        }
+      }, 500);
+    };
+
+    // on node open
+    $scope.open = function(props) {
+      $timeout.cancel(selectTimeout);
+      var node = $scope.nodes.get(props.nodes[0]);
+      $state.transitionTo(node.show, {id:node.dbid});
+    };
+
   }]);
 
-  /*
+  /****************************************************************************
    * Article Controllers
-   */
+   ****************************************************************************/
 
+  /**
+   * Article Index route
+   */
   app.controller('ArticlesIndexController', ['$scope', '$state', '$stateParams', 'ArticleFactory', 'Store',
     function($scope, $state, $stateParams, ArticleFactory, Store) {
 
@@ -112,6 +276,9 @@
 
   }]);
 
+  /**
+   * Article Show route
+   */
   app.controller('ArticlesShowController', ['$scope', '$stateParams', 'ArticleFactory',
     function($scope, $stateParams, ArticleFactory, testService) {
 
@@ -123,6 +290,8 @@
       $scope.article.modified = formatDateTime($scope.article.modified);
       $scope.articleMeta = response.meta;
       $scope.queryTime = response.$queryTime;
+      $scope.topicSort = $scope.topicSort || 'topic.share';
+      $scope.topicSortRev = typeof $scope.topicSortRev === 'undefined' ? false : $scope.topicSortRev;
 
       // calculate percentage share
       var topicShareSeries = [],
@@ -155,10 +324,13 @@
 
   }]);
 
-  /*
+  /****************************************************************************
    * Topic Controllers
-   */
+   ****************************************************************************/
 
+  /**
+   * Topic Index route
+   */
   app.controller('TopicsIndexController', ['$scope', '$stateParams', 'Store', 'TopicFactory',
     function($scope, $stateParams, Store, TopicFactory) {
 
@@ -188,8 +360,11 @@
 
   }]);
 
-  app.controller('TopicsShowController', ['$scope', '$stateParams', 'TopicFactory',
-    function($scope, $stateParams, TopicFactory) {
+  /**
+   * Topic Show route
+   */
+  app.controller('TopicsShowController', ['$scope', '$stateParams', '$timeout', 'TopicFactory',
+    function($scope, $stateParams, $timeout, TopicFactory) {
 
     TopicFactory.get({id: $stateParams.id}, function(response) {
       $scope.topic = response.data;
@@ -197,14 +372,43 @@
       $scope.topic.modified = formatDateTime($scope.topic.modified);
       $scope.topicMeta = response.meta;
       $scope.queryTime = response.$queryTime;
+      $scope.wordSort = $scope.wordSort || 'likeliness';
+      $scope.wordSortRev = typeof $scope.wordSortRev === 'undefined' ? true : $scope.wordSortRev;
+
+      $scope.startRename = function() {
+        $scope.origName = $scope.topic.name;
+        $scope.isRename = true;
+        $timeout(function() {
+          $('#topicName').select();
+        }, 0);
+      };
+
+      $scope.endRename = function(save) {
+        $scope.isRename = false;
+        if(save) {
+          // TODO implement
+        } else {
+          $scope.topic.name = $scope.origName;
+        }
+      };
+
+      $scope.keyup = function($event) {
+        if($event.which === 13 || $event.which === 27) {
+          $scope.endRename($event.which === 13);
+          $event.preventDefault();
+        }
+      };
     });
 
   }]);
 
-  /*
+  /****************************************************************************
    * Word Controllers
-   */
+   ****************************************************************************/
 
+  /**
+   * Word Index route
+   */
   app.controller('WordsIndexController', ['$scope', '$state', '$stateParams', 'Store', 'WordFactory',
     function($scope, $state, $stateParams, Store, WordFactory) {
 
@@ -235,6 +439,9 @@
 
   }]);
 
+  /**
+   * Word Show route
+   */
   app.controller('WordsShowController', ['$scope', '$stateParams', 'WordFactory',
     function($scope, $stateParams, WordFactory) {
 
@@ -247,10 +454,13 @@
 
   }]);
 
-  /*
+  /****************************************************************************
    * Directive Controllers
-   */
+   ****************************************************************************/
 
+  /**
+   * Pagination
+   */
   app.controller('PaginationController', ['$scope',
     function($scope) {
 
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index cd9abc0bcb3892d4a52af4daddabec8f3f433223..93059f3c3aea253225ed88aff6e02bdfe8ab90a4 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -57,176 +57,6 @@
     };
   });
 
-  app.directive('visNetwork', ['$state', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory',
-    function($state, $timeout, ArticleFactory, TopicFactory, WordFactory) {
-
-    return {
-      scope: {
-        ngModel: '=',
-        visType: '=',
-        visData: '=',
-        visColors: '=',
-        visShown: '='
-      },
-      transclude: true,
-      template: '<ng-transclude/><div class="fullsize navpadding" id="visgraph"></div>',
-      link: function($scope, $element) {
-        var id = 0,
-            ids = {},
-            container = $element.find("#visgraph")[0];
-
-        $scope.nodes = new vis.DataSet();
-        $scope.edges = new vis.DataSet();
-        $scope.data = {
-          nodes: $scope.nodes,
-          edges: $scope.edges
-        };
-
-        $scope.options = {
-          nodes: {
-            font: { size: 14 },
-            shape: 'dot',
-            borderWidth: 0
-          },
-          edges: {
-            color: {
-              highlight: '#f00'
-            }
-          },
-          layout: { randomSeed: 1 },
-          physics: {
-            barnesHut: {
-              springConstant: 0.008,
-              gravitationalConstant: -3500
-            }
-          }
-        };
-
-        var newNode = function(title, type, show, dbid, color, shape) {
-          ids[dbid] = ++id;
-          return {
-            id: id,
-            title: title,
-            label: title,
-            type: type,
-            show: show,
-            dbid: dbid,
-            shape: shape || 'dot',
-            color: {
-              background: color,
-              highlight: { background: color }
-            }
-          };
-        };
-
-        var topicNode = function(topic) {
-          topic = topic.topic || topic;
-          return newNode(topic.name, 'topic', 'topics.show', topic.id, $scope.visColors.topics, 'triangle');
-        };
-
-        var articleNode = function(article) {
-          return newNode(article.title, 'article', 'articles.show', article.id, $scope.visColors.articles, 'square');
-        };
-
-        var wordNode = function(word) {
-          return newNode(word.id, 'word', 'words.show', word.id, $scope.visColors.words);
-        };
-
-        // construct new nodes
-        var constructor = function(result, node, key, nodeFunction) {
-          if(result.data && (!key || result.data[key])) {
-            var data = key ? result.data[key] : result.data,
-                newNodes = [],
-                newEdges = [];
-            for(var i = 0; i < data.length; i++) {
-              var current = data[i];
-              if(ids.hasOwnProperty(current.id)) {
-                if(!$scope.nodes.get(ids[current.id]).loaded)
-                  newEdges.push({from:ids[current.id], to:node.id});
-              } else {
-                newNodes.push(nodeFunction(current));
-                newEdges.push({from:id, to:node.id});
-              }
-            }
-            if(newNodes.length) $scope.nodes.add(newNodes);
-            if(newEdges.length) $scope.edges.add(newEdges);
-          }
-        };
-        
-        // on node select
-        var selectTimeout;
-        $scope.select = function(props) {
-          $timeout.cancel(selectTimeout);
-          selectTimeout = $timeout(function() {
-            var node = $scope.nodes.get(props.nodes[0]);
-            if(node && !node.loaded) {
-              var loaded = false;
-              if(node.type === 'article' && $scope.visShown.topics) {
-                // node is article, load article to get topics
-                ArticleFactory.get({id:node.dbid}, function(res) {
-                  for(var i = 0; i < res.data.topics.length; i++)
-                    res.data.topics[i] = res.data.topics[i].topic;
-                  constructor(res, node, 'topics', topicNode);
-                });
-                loaded = true;
-              } else if(node.type === 'topic') {
-                // node is topic, load topic to get words and articles
-                if($scope.visShown.words) {
-                  TopicFactory.get({id:node.dbid}, function(res) {
-                    constructor(res, node, 'words', wordNode);
-                  });
-                  loaded = true;
-                }
-                if($scope.visShown.articles) {
-                  TopicFactory.articles({id:node.dbid}, function(res) {
-                    constructor(res, node, null, articleNode);
-                  });
-                  loaded = true;
-                }
-              } else if(node.type === 'word' && $scope.visShown.topics) {
-                // node is word, load word to get topics
-                WordFactory.get({id:node.dbid}, function(res) {
-                  constructor(res, node, 'topics', topicNode);
-                });
-                loaded = true;
-              }
-              if(loaded) {
-                node.loaded = true;
-                $scope.nodes.update(node);
-              }
-            }
-          }, 500);
-        };
-
-        // on node open
-        $scope.open = function(props) {
-          $timeout.cancel(selectTimeout);
-          var node = $scope.nodes.get(props.nodes[0]);
-          $state.transitionTo(node.show, {id:node.dbid});
-        };
-
-        // watch for changes to model
-        $scope.$watch('ngModel', function(newVal, oldVal) {
-          if(!newVal) return;
-
-          // add root node
-          if($scope.visType === 'articles')
-            $scope.nodes.add([articleNode($scope.ngModel)]);
-          else if($scope.visType === 'topics')
-            $scope.nodes.add([topicNode($scope.ngModel)]);
-          else if($scope.visType === 'words')
-            $scope.nodes.add([wordNode($scope.ngModel)]);
-          ids[$scope.ngModel.id] = id;
-
-          // create graph
-          $scope.visGraph = new vis.Network(container, $scope.data, $scope.options);
-          $scope.visGraph.on('selectNode', $scope.select);
-          $scope.visGraph.on('doubleClick', $scope.open);
-        });
-      }
-    };
-  }]);
-
   app.directive('highcharts', function() {
     return {
       scope: {
@@ -289,4 +119,72 @@
     }
   });
 
+  app.directive('storeValue', ['Store', function(Store) {
+    return {
+      restrict: 'A',
+      require: 'ngModel',
+      link: function($scope, $elem, $attrs, $ctrl) {
+        if(!$attrs.storeValue) {
+          console.log("no store key given");
+          return;
+        }
+        var value = Store($attrs.storeValue);
+        if(typeof value !== 'undefined') {
+          $ctrl.$setViewValue(value);
+          $ctrl.$render();
+        }
+        $ctrl.$viewChangeListeners.push(function() {
+          Store($attrs.storeValue, $ctrl.$viewValue);
+        });
+      }
+    };
+  }]);
+
+  app.directive('bsDropdown', function() {
+    return {
+      templateUrl: 'html/directives/dropdown.html',
+      transclude: true,
+      replace: true,
+      link: function($scope, $elem, $attrs) {
+        $scope.dropdownId = randomId();
+        $scope.label = $attrs.label;
+        $scope.align = 'dropdown-menu-left';
+        if($attrs.align === 'right')
+          $scope.align = 'dropdown-menu-right';
+      }
+    };
+  });
+
+  app.directive('sortBy', ['Store', function(Store) {
+    return {
+      scope: {
+        sortBy: '@',
+        sortType: '=',
+        sortReverse: '=',
+        storeKey: '@sortType'
+      },
+      restrict: 'A',
+      transclude: true,
+      template: '<span ng-click="click()"><ng-transclude/><span ng-show="sortType == sortBy" class="caret" ng-class="{\'caret-up\':sortReverse}"></span></span>',
+      link: function($scope, $elem, $attrs) {
+        if(!$attrs.sortBy) {
+          console.log('no sort by key given');
+          return;
+        }
+        var value = Store($scope.storeKey);
+        if(value) {
+          value = value.split(',');
+          $scope.sortType = value[0];
+          $scope.sortReverse = value[1] === 'true';
+        }
+        $scope.sortBy = $attrs.sortBy;
+        $scope.click = function() {
+          $scope.sortType = $scope.sortBy;
+          $scope.sortReverse = !$scope.sortReverse;
+          Store($scope.storeKey, $scope.sortType + ',' + $scope.sortReverse);
+        };
+      }
+    };
+  }]);
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/js/helpers.js b/vipra-ui/app/js/helpers.js
index dacff035c3e805cb785d4e5b980162a1cc93e37f..31c3ef07289f5754dae26222e3726269e171fead 100644
--- a/vipra-ui/app/js/helpers.js
+++ b/vipra-ui/app/js/helpers.js
@@ -23,6 +23,10 @@
     return '<span class="initial">' + text.substring(0, 1) + "</span>" + text.substring(1);
   };
 
+  window.randomId = function() {
+    return 'id' + Math.random().toString(36).substring(7);
+  };
+
   String.prototype.ellipsize = function(max) {
     max = max || 20;
     if(this.length > max) {
@@ -31,6 +35,15 @@
     return this;
   };
 
+  String.prototype.multiline = function(max) {
+    return this.split(new RegExp("((?:\\w+ ){" + max + "})", "g")).filter(Boolean).join("\n");
+  };
+
+  if(typeof String.prototype.startsWith === 'undefined')
+    String.prototype.startsWith = function(start) {
+      return this.lastIndexOf(start, 0) === 0;
+    };
+
   window.console = window.console || {
     log: function () {}
   };
diff --git a/vipra-ui/app/js/services.js b/vipra-ui/app/js/services.js
deleted file mode 100644
index 5f8e95703ea98dc8ae766127dd7a21420231b528..0000000000000000000000000000000000000000
--- a/vipra-ui/app/js/services.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/******************************************************************************
- * Vipra Application
- * Services
- ******************************************************************************/
-(function() {
-
-  var app = angular.module('vipra.services', []);
-
-})();
\ No newline at end of file
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index 08c38cef3d7ed8923523c7008f540092da911d8e..a1be97047b06d257b015252a398026ede6957952 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -129,8 +129,14 @@ ul.dashed {
   left: 10px;
   font-weight: bold;
   padding: 10px;
-  border: 1px solid #aaa;
-  border-radius: 3px;
+
+  label {
+    margin: 0;
+  }
+
+  label + label {
+    padding-left: 5px;
+  }
 }
 
 .initial {
@@ -167,7 +173,6 @@ ul.dashed {
 
 .navpadding {
   padding-top: 50px;
-  padding-bottom: 50px;
 }
 
 .form-control-inline {
@@ -179,6 +184,36 @@ ul.dashed {
   z-index: 9999;
 }
 
+.item-actions {
+  padding: 5px 0 0 10px;
+}
+
+.row {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+}
+
+.caret.caret-up {
+  content: "";
+  border-top: 0;
+  border-bottom: 4px dashed;
+}
+
+.table.table-morecondensed > tbody > tr > td {
+  padding: 0;
+}
+
+.page-header h1 {
+  min-height: 48px;
+}
+
+[sort-by] {
+  .noselect;
+  cursor: pointer;
+}
+
 #nprogress .spinner {
   display: none;
 }
diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json
index 1efe46c3299924589c13c787c661a50ef095b070..da39b5d3e8c1ad9b8d75a488378541f38dae2b43 100644
--- a/vipra-ui/bower.json
+++ b/vipra-ui/bower.json
@@ -23,7 +23,6 @@
     "angular-resource": "^1.5.0",
     "angular-ui-router": "^0.2.17",
     "angular-sanitize": "^1.5.0",
-    "angular-bootstrap": "^1.1.2",
     "highcharts": "^4.2.2",
     "nprogress": "^0.2.0",
     "vis": "https://github.com/almende/vis.git#^4.14.0"
diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js
index 7b4035879b6efa6731e3da89373b5738a12d6472..3b4c63e2d9db7abc4027702cb6fcb52246a53747 100644
--- a/vipra-ui/gulpfile.js
+++ b/vipra-ui/gulpfile.js
@@ -14,7 +14,6 @@ var assets = {
     'bower_components/angular-resource/angular-resource.min.js',
     'bower_components/angular-sanitize/angular-sanitize.min.js',
     'bower_components/angular-ui-router/release/angular-ui-router.min.js',
-    'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
     'bower_components/bootstrap/dist/js/bootstrap.min.js',
     'bower_components/highcharts/highcharts.js',
     'bower_components/vis/dist/vis.min.js',