namespace-browser: Adding "Delete" functionality for mountpoint and
also changing the reload action not to reload the whole page and just
clear cache and refresh current view.

Closes https://github.com/vanadium/browser/issues/63

Change-Id: I2c93212808ea248819df6206bcbc2492f265007a
diff --git a/src/app.js b/src/app.js
index b61a882..3bd8ef3 100644
--- a/src/app.js
+++ b/src/app.js
@@ -9,12 +9,13 @@
 var onboarding = require('./onboarding');
 var router = require('./router');
 var registerItemPlugins = require('./item-plugins/register-plugins');
-var debug = require('./components/debug/index');
-var browse = require('./components/browse/index');
-var error = require('./components/error/index');
-var help = require('./components/help/index');
-var viewport = require('./components/viewport/index');
-var userAccount = require('./components/user-account/index');
+var debug = require('./components/debug');
+var browse = require('./components/browse');
+var error = require('./components/error');
+var help = require('./components/help');
+var viewport = require('./components/viewport');
+var views = require('./components/browse/views');
+var userAccount = require('./components/user-account');
 var namespaceService = require('./services/namespace/service');
 var sampleWorld = require('./services/sample-world');
 var stateService = require('./services/state/service');
@@ -120,7 +121,13 @@
    * }
    * is expected as data for the event
    */
-  'navigate'
+  'navigate',
+
+  /*
+   * Event indicating a request to reload the current namespace
+   * The current namespace will be passed as data into the handlers.
+   */
+  'reload'
 ]);
 events.browse = browseComponent.events;
 events.help = helpComponent.events;
@@ -161,12 +168,39 @@
   events.browse.error(onError);
   events.browse.toast(onToast);
 
+  events.navigation.reload(onReload);
+
   // Hook up external help events.
   events.help.navigate = events.navigation.navigate;
   events.help.error(onError);
 }
 
 /*
+ * Reload the views for the current namespace
+ */
+function onReload() {
+  var namespace = state.browse.namespace();
+  log.debug('reloading', namespace);
+
+  // clear the service cache
+  namespaceService.clearCache(namespace);
+
+  // tell views to clear their caches
+  views.clearCache(state.browse.views, namespace);
+
+  // navigate to the namespace again
+  // TODO(aghassemi) Ideally we only reset the selected item if the old one
+  // no longer is in the view, but that's a bit tricky and depends on
+  // https://github.com/vanadium/browser/issues/81
+  state.browse.selectedItemName.set(namespace);
+  events.navigation.navigate({
+    path: browseRoute.createUrl(state.browse(), {
+      namespace: namespace
+    })
+  });
+}
+
+/*
  * Given an error, navigate to the error page and display that error.
  */
 function onError(err) {
@@ -291,5 +325,4 @@
  */
 function onVanadiumCrash(crashErr) {
   events.browse.error(crashErr);
-}
-
+}
\ No newline at end of file
diff --git a/src/components/browse/index.css b/src/components/browse/index.css
index a901c4c..edef1ac 100644
--- a/src/components/browse/index.css
+++ b/src/components/browse/index.css
@@ -137,6 +137,7 @@
 
 .namespace-box core-tooltip.icontooltip {
   margin-top: var(--size-space-xxsmall);
+  align-self: center;
 }
 
 .namespace-box core-tooltip.nstooltip {
diff --git a/src/components/browse/index.js b/src/components/browse/index.js
index 9898ba7..4cf3b01 100644
--- a/src/components/browse/index.js
+++ b/src/components/browse/index.js
@@ -351,8 +351,10 @@
       }, [
         h('div.resize-handle', {
           'ev-mousedown': function(e) {
-            browseEvents.slideSidePanel({ rawEvent: e,
-                collapsed: browseState.sidePanelCollapsed });
+            browseEvents.slideSidePanel({
+              rawEvent: e,
+              collapsed: browseState.sidePanelCollapsed
+            });
           }
         }),
         sideView
@@ -433,9 +435,7 @@
             'icon': 'refresh',
             'label': 'Reload'
           },
-          'ev-click': function() {
-            location.reload();
-          }
+          'ev-click': mercury.send(navEvents.reload)
         })
       ),
       h('core-tooltip.nstooltip', {
@@ -621,17 +621,17 @@
     });
     breadCrumbs.push(h('li.breadcrumb-item.relative-name' +
       (parentParts.length ? '.breadcrumb-item-prefix' : ''), [
-      //TODO(aghassemi) refactor link generation code
-      h('a', {
-        'href': rootUrl,
-        'ev-click': mercury.event(navEvents.navigate, {
-          path: rootUrl
-        })
-      }, '<Home>')
-    ]));
+        //TODO(aghassemi) refactor link generation code
+        h('a', {
+          'href': rootUrl,
+          'ev-click': mercury.event(navEvents.navigate, {
+            path: rootUrl
+          })
+        }, '<Home>')
+      ]));
   }
 
-  parentParts.pop();  // remove last part (current view root)
+  parentParts.pop(); // remove last part (current view root)
 
   for (var i = 0; i < namespaceParts.length; i++) {
     var namePart = namespaceParts[i].trim();
@@ -707,19 +707,19 @@
     }
 
     function slideEnd(e) { // release
-      window.removeEventListener('mouseup', slideEnd);
-      window.removeEventListener('mousemove', slideMove);
-      drawer.querySelector('::shadow core-selector').
-      classList.add('transition');
-      var drawerWidth = drawer.getAttribute('drawerWidth');
+        window.removeEventListener('mouseup', slideEnd);
+        window.removeEventListener('mousemove', slideMove);
+        drawer.querySelector('::shadow core-selector').
+        classList.add('transition');
+        var drawerWidth = drawer.getAttribute('drawerWidth');
 
-      // async call to persist the drawer width
-      stateService.saveSidePanelWidth(drawerWidth);
+        // async call to persist the drawer width
+        stateService.saveSidePanelWidth(drawerWidth);
 
-      state.sidePanelWidth.set(drawerWidth);
-      state.sidePanelCollapsed.set(false);
-      fireResizeEvent(null);
-    } // end slideEnd
+        state.sidePanelWidth.set(drawerWidth);
+        state.sidePanelCollapsed.set(false);
+        fireResizeEvent(null);
+      } // end slideEnd
   }); // end events.slideSidePanel
 
   function fireResizeEvent(e) { // resize on end animation
@@ -733,4 +733,4 @@
     }
   }
 
-}
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/display-item-details.js b/src/components/browse/item-details/display-item-details.js
index c7bff09..1e50a62 100644
--- a/src/components/browse/item-details/display-item-details.js
+++ b/src/components/browse/item-details/display-item-details.js
@@ -29,11 +29,6 @@
 function displayItemDetails(state, events, data) {
   var name = data.name;
 
-  // Return if we are already on that item.
-  if (isCurrentlySelected()) {
-    return;
-  }
-
   lastRequestedName = name;
 
   state.put('plugins', mercury.array([]));
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index 73b0d41..c4c2cab 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -102,8 +102,6 @@
 
   events.serverDetails = serverDetailsComponent.events;
   events.mountPointDetails = mountPointDetailsComponent.events;
-  events.serverDetails.toast = events.toast;
-  events.mountPointDetails.toast = events.toast;
   wireUpEvents(state, events);
 
   return {
@@ -347,19 +345,18 @@
       'tabkey': tabKey
     },
     'ev-click': new polymerEvent(function(data) {
-        events.tabSelected({
-          tabKey: tabKey
-        });
-      })
-    }, [
-      h('core-icon.tab-icon', {
-        attributes: {
-          'icon': icon,
-          'alt': '' // because we have the title beside it
-        }
-      }), title
-    ]
-  );
+      events.tabSelected({
+        tabKey: tabKey
+      });
+    })
+  }, [
+    h('core-icon.tab-icon', {
+      attributes: {
+        'icon': icon,
+        'alt': '' // because we have the title beside it
+      }
+    }), title
+  ]);
 }
 
 /*
@@ -386,9 +383,15 @@
 
 // Wire up events that we know how to handle
 function wireUpEvents(state, events) {
+  events.serverDetails.toast = function(data) {
+    events.toast(data);
+  };
+  events.mountPointDetails.toast = function(data) {
+    events.toast(data);
+  };
   events.bookmark(bookmark.bind(null, state, events));
   events.displayItemDetails(displayItemDetails.bind(null, state, events));
   events.tabSelected(function(data) {
     state.selectedTabKey.set(data.tabKey);
   });
-}
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/mount-point-details/display-mountpoint-details.js b/src/components/browse/item-details/mount-point-details/display-mountpoint-details.js
index d418736..2bd3733 100644
--- a/src/components/browse/item-details/mount-point-details/display-mountpoint-details.js
+++ b/src/components/browse/item-details/mount-point-details/display-mountpoint-details.js
@@ -22,11 +22,6 @@
   var itemObs = data.itemObs;
   var name = itemObs().objectName;
 
-  // Return if we are already on that item.
-  if (isCurrentlySelected()) {
-    return;
-  }
-
   lastRequestedName = name;
 
   state.put('item', itemObs);
diff --git a/src/components/browse/item-details/mount-point-details/index.js b/src/components/browse/item-details/mount-point-details/index.js
index 061eca9..11a47b5 100644
--- a/src/components/browse/item-details/mount-point-details/index.js
+++ b/src/components/browse/item-details/mount-point-details/index.js
@@ -6,10 +6,16 @@
 var insertCss = require('insert-css');
 
 var displayMountPointDetails = require('./display-mountpoint-details');
+var mountPointManager = require('./manage-mountpoint');
 
+var dialogClickHook = require('../../../../lib/mercury/dialog-click-hook');
 var FieldItem = require('../field-item');
 var ErrorBox = require('../../../error/error-box');
 
+var log = require('../../../../lib/log')(
+  'components:browse:item-details:mount-point'
+);
+
 var css = require('./index.css');
 var h = mercury.h;
 
@@ -60,7 +66,7 @@
     permissions: mercury.value(null),
 
     /*
-     * whether user is even authorized to see the permission for the mount point
+     * Whether user is even authorized to see the permission for the mount point
      * @type {boolean}
      */
     notAuthorizedToSeePermissions: mercury.value(false),
@@ -69,14 +75,43 @@
      * The objectAddresses as resolveToMounttable
      * @type {mercury.array<string>}
      */
-    objectAddresses: mercury.array([])
+    objectAddresses: mercury.array([]),
+
+    /*
+     * Whether we should render a dialog prompting for an action.
+     * @type {boolean}
+     */
+    promptAction: mercury.value(false),
+
+    /*
+     * The text for the prompt.
+     * @type {string}
+     */
+    promptActionText: mercury.value(''),
+
+    /*
+     * The text for the positive button of the prompt.
+     * @type {string}
+     */
+    promptActionButtonText: mercury.value(''),
+
+    /*
+     * The event handler that will be called when confirmed.
+     * @type {function}
+     */
+    promptActionCallback: mercury.value()
 
   });
 
   var events = mercury.input([
-    'toast'
+    'toast',
+    'promptDeleteMountPoint',
+    'promptCanceled',
+    'promptConfirmed'
   ]);
 
+  wireUpEvents(state, events);
+
   return {
     state: state,
     events: events
@@ -97,6 +132,7 @@
     displayItems.push(renderNameField(state));
     displayItems.push(renderObjectAddressesField(state));
     displayItems.push(renderPermissionsField(state));
+    displayItems.push(renderActionsField(state, events, navEvents));
 
     content.push(h('div', displayItems));
   }
@@ -156,12 +192,84 @@
 }
 
 /*
+ * Renders the mountpoint actions.
+ */
+function renderActionsField(state, events, navEvents) {
+  var actions = [
+    renderDeleteAction(state, events, navEvents)
+  ];
+
+  var filteredActions = actions.filter(function(a) {
+    return !!a;
+  });
+
+  if (filteredActions.length === 0) {
+    return;
+  }
+
+  return [
+    FieldItem.render('Manage', h('div', filteredActions)),
+    renderPrompt(state, events)
+  ];
+}
+
+/*
+ * Renders a modal prompt dialog if an action is taking place to confirm.
+ */
+function renderPrompt(state, events) {
+  return h('paper-action-dialog', {
+    attributes: {
+      'autoCloseDisabled': true,
+      'layered': true,
+      'backdrop': true,
+    },
+    'opened': state.promptAction
+  }, [
+    h('p', state.promptActionText),
+
+    h('paper-button', {
+      attributes: {
+        'dismissive': true,
+      },
+      'click-hook': dialogClickHook(mercury.send(events.promptCanceled))
+    }, 'Cancel'),
+
+    h('paper-button', {
+      attributes: {
+        'affirmative': true,
+        'autofocus': true
+      },
+      'click-hook': dialogClickHook(mercury.send(events.promptConfirmed))
+    }, state.promptActionButtonText)
+  ]);
+}
+
+/*
+ * Renders the mountpoint delete action.
+ */
+function renderDeleteAction(state, events, navEvents) {
+  /* TODO(aghassemi) We really should only render items user has access to.
+   * This was attempted by trying to match remoteBlessings with peerBlessings
+   * but we intentionally do not expose blessing names for peerBlessings so
+   * approach did not work.
+   * This needs https://github.com/vanadium/issues/issues/210 to be fixed first.
+   */
+  var action = h('paper-button', {
+    'ev-click': mercury.send(events.promptDeleteMountPoint, {
+      cb: navEvents.reload,
+      name: state.itemName
+    })
+  }, 'Delete');
+
+  return action;
+}
+
+/*
  * Formats a permissions object to string
  * TODO(aghassemi): we need a nicer permission formatter
  * @param {vanadium.security.Permissions} perms
  */
 function formatPermissions(perms) {
-  //
   var results = [];
   perms.forEach(function(p, key) {
     results.push(
@@ -207,4 +315,36 @@
   }
 
   return h('div', results);
+}
+
+// Wire up events that we know how to handle
+function wireUpEvents(state, events) {
+  events.promptDeleteMountPoint(function(data) {
+    state.promptAction.set(true);
+    state.promptActionText.set(
+      'Are you sure you want to delete ' + data.name + ' ?'
+    );
+    state.promptActionButtonText.set('Delete');
+    state.promptActionCallback.set(deleteMountPoint.bind(null, data));
+  });
+
+  events.promptCanceled(function() {
+    state.promptAction.set(false);
+  });
+
+  events.promptConfirmed(function() {
+    var cb = state.promptActionCallback();
+    cb();
+    state.promptAction.set(false);
+  });
+
+  function deleteMountPoint(data) {
+    mountPointManager.deleteMountPoint(state, events).then(function() {
+      if (data.cb) {
+        data.cb();
+      }
+    }, function(err) {
+      log.error('Could not delete mount point', err);
+    });
+  }
 }
\ No newline at end of file
diff --git a/src/components/browse/item-details/mount-point-details/manage-mountpoint.js b/src/components/browse/item-details/mount-point-details/manage-mountpoint.js
new file mode 100644
index 0000000..f292a90
--- /dev/null
+++ b/src/components/browse/item-details/mount-point-details/manage-mountpoint.js
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var namespaceService = require('../../../../services/namespace/service');
+
+var log = require('../../../../lib/log')(
+  'components:browse:item-details:mount-point:manage-mountpoint'
+);
+
+module.exports = {
+  deleteMountPoint: deleteMountPoint
+};
+
+/*
+ * Delete a given mountpoint
+ */
+ //TODO(aghassemi) Prompt for confirmation
+function deleteMountPoint(state, events) {
+  var name = state.itemName;
+  return namespaceService.deleteMountPoint(name).then(function() {
+    events.toast({
+      text: name + ' deleted successfully'
+    });
+  }).catch(function(err) {
+    var errText = 'Could not delete ' + name;
+    if (err && err.id === 'v.io/v23/verror.NoAccess') {
+      errText = 'Not authorized to delete ' + name;
+    }
+
+    log.error(errText, name, err);
+    events.toast({
+      text: errText,
+      type: 'error'
+    });
+
+    return Promise.reject(err);
+  });
+}
diff --git a/src/components/browse/views/index.js b/src/components/browse/views/index.js
index 2384b70..e1dc96f 100644
--- a/src/components/browse/views/index.js
+++ b/src/components/browse/views/index.js
@@ -18,6 +18,7 @@
 module.exports.render = render;
 module.exports.load = load;
 module.exports.trySetViewType = trySetViewType;
+module.exports.clearCache = clearCache;
 
 var VALID_VIEW_TYPES = ['grid', 'tree', 'visualize'];
 
@@ -99,6 +100,14 @@
 
   if (state.viewType() === 'tree') {
     TreeView.expand(state.tree, namespace);
+    // During a reload, some tree nodes may already have been expanded.
+    // If so, call expand to re-glob their children.
+    var expandedNames = state.tree.expandedMap();
+    for (var name in expandedNames) {
+      if (expandedNames[name] === true) {
+        TreeView.expand(state.tree, name);
+      }
+    }
     return namespaceService.getNamespaceItem(namespace)
       .then(function(item) {
         state.tree.put('rootItem', item);
@@ -161,4 +170,11 @@
     default:
       log.error('Unsupported viewType: ' + state.viewType);
   }
+}
+
+// Clears any locally cached data
+function clearCache(state, namespace) {
+  state.put('items', mercury.array([]));
+  TreeView.clearCache(state.tree, namespace);
+  VisualizeView.clearCache();
 }
\ No newline at end of file
diff --git a/src/components/browse/views/tree-view/index.js b/src/components/browse/views/tree-view/index.js
index 90f69b8..6b1742e 100644
--- a/src/components/browse/views/tree-view/index.js
+++ b/src/components/browse/views/tree-view/index.js
@@ -6,6 +6,8 @@
 var insertCss = require('insert-css');
 var extend = require('extend');
 
+var namespaceService = require('../../../../services/namespace/service');
+
 var polymerEvent = require('../../../../lib/mercury/polymer-event');
 var expand = require('./expand');
 var getServiceIcon = require('../../get-service-icon');
@@ -16,6 +18,7 @@
 module.exports = create;
 module.exports.render = render;
 module.exports.expand = expand;
+module.exports.clearCache = clearCache;
 
 function create() {
 
@@ -138,3 +141,22 @@
     });
   });
 }
+
+/*
+ * Given a name, it clears local cache of any items that match the name or
+ * are a suffix of it.
+ */
+function clearCache(state, namespace) {
+  deleteFromVarHashByPrefix(state.childrenMap, namespace);
+}
+
+function deleteFromVarHashByPrefix(varhash, prefix) {
+  var childrenKeys = Object.keys(varhash());
+  childrenKeys.forEach(function(ck) {
+    // Ideally we want a transaction here but VarHash does not support it yet
+    // https://github.com/nrw/observ-varhash/issues/15
+    if (namespaceService.prefixes(prefix, ck)) {
+      varhash.delete(ck);
+    }
+  });
+}
\ No newline at end of file
diff --git a/src/components/browse/views/visualize-view/index.js b/src/components/browse/views/visualize-view/index.js
index d0a76e8..3d384e2 100644
--- a/src/components/browse/views/visualize-view/index.js
+++ b/src/components/browse/views/visualize-view/index.js
@@ -10,83 +10,62 @@
 var browseRoute = require('../../../../routes/browse');
 var getServiceIcon = require('../../get-service-icon');
 
-var log = require('../../../../lib/log'
-    )('components:browse:items:visualize-view');
+var log = require('../../../../lib/log')(
+  'components:browse:items:visualize-view'
+);
 
 var css = require('./index.css');
 var h = mercury.h;
 
 module.exports = create;
 module.exports.render = render;
-
-// Maximum number of levels that are automatically loaded below the root
-// var MAX_AUTO_LOAD_DEPTH = 3;
+module.exports.clearCache = clearCache;
 
 var DURATION = 500; // d3 animation duration
 var STAGGER = 5; // mS delay for each node
 var MIN_ZOOM = 0.5; // minimum zoom allowed
-var MAX_ZOOM = 20;  // maximum zoom allowed
+var MAX_ZOOM = 20; // maximum zoom allowed
 var SYMBOL_STROKE_COLOR = '#00838F';
-// var HAS_CHILDREN_COLOR = 'gray';
-// var NO_CHILDREN_COLOR = 'white';
-var SELECTED_COLOR = '#E65100';  // color of selected node
-var ZOOM_INC = 0.06;  // zoom factor per animation frame
-var PAN_INC = 3;  //  pan per animation frame
-var ROT_INC = 0.5;  // rotation per animation frame
+var SELECTED_COLOR = '#E65100'; // color of selected node
+var ZOOM_INC = 0.06; // zoom factor per animation frame
+var PAN_INC = 3; //  pan per animation frame
+var ROT_INC = 0.5; // rotation per animation frame
 var RELATIVE_ROOT = '<Home>';
 
-var widget; // instance of D3Widget
-var networkElem;  // DOM element for visualization
-
-var selNode;  // currently selected node
-var selectItem;  // function to select item in app
-
-var width, height;  // size of the visualization diagram
-
-var curX, curY, curR, curZ; // transforms (x, y, rotate, zoom)
-var modZ; // modified zoom for nodes and text
-
-var diagonal; // d3 diagonal projection for use by the node paths
-var treeD3; // d3 tree layout
-var svgBase, svgGroup;  // svg elements
-
-var root;  // data trees
-var rootIndex = {}; // index id to nodes
-
 // keyboard key codes
-var KEY_PLUS = 187;     // + (zoom in)
-var KEY_MINUS = 189;    // - (zoom out)
-var KEY_PAGEUP = 33;    // (rotate CCW)
-var KEY_PAGEDOWN = 34;  // (rotate CW)
-var KEY_LEFT = 37;      // left arrow
-var KEY_UP = 38;        // up arrow
-var KEY_RIGHT = 39;     // right arrow
-var KEY_DOWN = 40;      // down arrow
-var KEY_SPACE = 32;     // (expand node)
-var KEY_RETURN = 13;    // (expand tree)
-var KEY_HOME = 36;      // (center root)
-var KEY_END = 35;       // (center selection)
+var KEY_PLUS = 187; // + (zoom in)
+var KEY_MINUS = 189; // - (zoom out)
+var KEY_PAGEUP = 33; // (rotate CCW)
+var KEY_PAGEDOWN = 34; // (rotate CW)
+var KEY_LEFT = 37; // left arrow
+var KEY_UP = 38; // up arrow
+var KEY_RIGHT = 39; // right arrow
+var KEY_DOWN = 40; // down arrow
+var KEY_SPACE = 32; // (expand node)
+var KEY_RETURN = 13; // (expand tree)
+var KEY_HOME = 36; // (center root)
+var KEY_END = 35; // (center selection)
 
-function create() { }
+function create() {}
+
+var widget;
+
+function clearCache() {
+  widget = null;
+}
 
 function render(itemsState, browseState, browseEvents, navEvents) {
   insertCss(css);
 
-  // handle changed selection
-  selNode = rootIndex[browseState.selectedItemName] || selNode;
-
-  browseInto.browseState = browseState;
-  browseInto.navEvents = navEvents;
-
-  if (widget === undefined) {
-    widget = new D3Widget(browseState, browseEvents);
+  if (!widget) {
+    widget = createWidget(browseState, browseEvents, navEvents);
   } else {
     widget.update(browseState, browseEvents);
   }
 
   return [
     widget,
-    h('div.vismenu', {  // visualization menu
+    h('div.vismenu', { // visualization menu
     }, [
       h('paper-fab.zoom', {
         attributes: {
@@ -96,8 +75,8 @@
           title: 'Zoom In (+)',
           'aria-label': 'zoom in'
         },
-        'ev-down': polydown.bind(undefined, KEY_PLUS),
-        'ev-up': polyup.bind(undefined, KEY_PLUS)
+        'ev-down': widget.polydown.bind(undefined, KEY_PLUS),
+        'ev-up': widget.polyup.bind(undefined, KEY_PLUS)
       }),
       h('paper-fab.zoom', {
         attributes: {
@@ -106,8 +85,8 @@
           title: 'Zoom Out (\u2212)',
           'aria-label': 'zoom out'
         },
-        'ev-down': polydown.bind(undefined, KEY_MINUS),
-        'ev-up': polyup.bind(undefined, KEY_MINUS),
+        'ev-down': widget.polydown.bind(undefined, KEY_MINUS),
+        'ev-up': widget.polyup.bind(undefined, KEY_MINUS),
       }),
       h('paper-fab.rotate', {
         attributes: {
@@ -116,8 +95,8 @@
           title: 'Rotate CCW (Page Up)',
           'aria-label': 'rotate counterclockwise'
         },
-        'ev-down': polydown.bind(undefined, KEY_PAGEUP),
-        'ev-up': polyup.bind(undefined, KEY_PAGEUP)
+        'ev-down': widget.polydown.bind(undefined, KEY_PAGEUP),
+        'ev-up': widget.polyup.bind(undefined, KEY_PAGEUP)
       }),
       h('paper-fab.rotate', {
         attributes: {
@@ -126,8 +105,8 @@
           title: 'Rotate CW (Page Down)',
           'aria-label': 'rotate clockwise'
         },
-        'ev-down': polydown.bind(undefined, KEY_PAGEDOWN),
-        'ev-up': polyup.bind(undefined, KEY_PAGEDOWN)
+        'ev-down': widget.polydown.bind(undefined, KEY_PAGEDOWN),
+        'ev-up': widget.polyup.bind(undefined, KEY_PAGEDOWN)
       }),
       h('paper-fab.expand', {
         attributes: {
@@ -136,321 +115,355 @@
           title: 'Load +1 Level (space bar)',
           'aria-label': 'load +1 level'
         },
-        'ev-click': menu.bind(undefined, KEY_SPACE, false)
+        'ev-click': widget.menu.bind(undefined, KEY_SPACE, false)
       })
-    ] ),
+    ]),
     h('paper-shadow.contextmenu', { // context menu
-          attributes: {
-            z: 3  // height above background
-          }
-      }, [  // context menu
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_RETURN, false),
-            'ev-mousedown': menu.bind(undefined, KEY_RETURN, false) },
-            [ h('div.ecnode', 'Expand Node'), h('div.ksc', 'Return') ]),
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_RETURN, true),
-            'ev-mousedown': menu.bind(undefined, KEY_RETURN, true) },
-            [ h('div', 'Show Loaded'), h('div.ksc', '\u21E7 Return') ]),
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_SPACE, false),
-            'ev-mousedown': menu.bind(undefined, KEY_SPACE, false) },
-            [ h('div', 'Load +1 Level'), h('div.ksc', 'space bar') ]),
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_SPACE, true),
-            'ev-mousedown': menu.bind(undefined, KEY_SPACE, true) },
-            [ h('div', 'Browse Into'), h('div.ksc', '\u21E7 space bar') ]),
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_END, false),
-            'ev-mousedown': menu.bind(undefined, KEY_END, false) },
-            [ h('div', 'Center Selected'), h('div.ksc', 'End') ]),
-        h('paper-item',
-          { 'ev-mouseup': menu.bind(undefined, KEY_HOME, false),
-            'ev-mousedown': menu.bind(undefined, KEY_HOME, false) },
-            [ h('div', 'Center Root'), h('div.ksc', 'Home') ])
-    ] )
+      attributes: {
+        z: 3 // height above background
+      }
+    }, [ // context menu
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_RETURN, false),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_RETURN, false)
+      }, [h('div.ecnode', 'Expand Node'), h('div.ksc', 'Return')]),
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_RETURN, true),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_RETURN, true)
+      }, [h('div', 'Show Loaded'), h('div.ksc', '\u21E7 Return')]),
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_SPACE, false),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_SPACE, false)
+      }, [h('div', 'Load +1 Level'), h('div.ksc', 'space bar')]),
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_SPACE, true),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_SPACE, true)
+      }, [h('div', 'Browse Into'), h('div.ksc', '\u21E7 space bar')]),
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_END, false),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_END, false)
+      }, [h('div', 'Center Selected'), h('div.ksc', 'End')]),
+      h('paper-item', {
+        'ev-mouseup': widget.menu.bind(undefined, KEY_HOME, false),
+        'ev-mousedown': widget.menu.bind(undefined, KEY_HOME, false)
+      }, [h('div', 'Center Root'), h('div.ksc', 'Home')])
+    ])
   ];
 }
 
-// Constructor for mercury widget for d3 element
-function D3Widget(browseState, browseEvents) {
-  this.browseState = browseState;
-  this.browseEvents = browseEvents;
-}
+function createWidget(browseState, browseEvents, navEvents) {
 
-D3Widget.prototype.type = 'Widget';
+  var networkElem; // DOM element for visualization
 
-D3Widget.prototype.init = function() {
-  if (!networkElem) {
-    networkElem = document.createElement('div');
-    networkElem.className = 'network';
-    networkElem.setAttribute('tabindex', 0);  // allow focus
-    selectItem = this.browseEvents.selectItem.bind(this.browseEvents);
-    requestAnimationFrame(initD3);
+  var selNode; // currently selected node
+  var selectItem; // function to select item in app
+
+  var width, height; // size of the visualization diagram
+
+  var curX, curY, curR, curZ; // transforms (x, y, rotate, zoom)
+  var modZ; // modified zoom for nodes and text
+
+  var diagonal; // d3 diagonal projection for use by the node paths
+  var treeD3; // d3 tree layout
+  var svgBase, svgGroup; // svg elements
+
+  var root; // data trees
+  var rootIndex = {}; // index id to nodes
+
+  browseInto.browseState = browseState;
+  browseInto.navEvents = navEvents;
+
+  // Constructor for mercury widget for d3 element
+  function D3Widget(browseState, browseEvents) {
+    this.browseState = browseState;
+    this.browseEvents = browseEvents;
   }
 
-  // wrap in a new element, needed for Mercury vdom to patch properly.
-  var wrapper = document.createElement('div');
-  wrapper.className = 'networkParent';
-  wrapper.appendChild(networkElem);
+  D3Widget.prototype.type = 'Widget';
 
-  requestAnimationFrame(this.updateRoot.bind(this));
-
-  return wrapper;
-};
-
-// Keep track of previous namespace that was browsed to so we can
-// know when navigating to a different namespace happens.
-var previousNamespace;
-
-D3Widget.prototype.update = function(browseState, browseEvents) {
-  this.browseState = browseState;
-  this.browseEvents = browseEvents;
-
-  // check to see if window was resized while we were away
-  if (width !== networkElem.offsetWidth) { resize(); }
-
-  this.updateRoot();
-
-  networkElem.focus();
-};
-
-// build new data tree
-D3Widget.prototype.updateRoot = function() {
-  var rootNodeId = this.browseState.namespace;
-
-  if (previousNamespace !== rootNodeId) {
-
-    previousNamespace = rootNodeId;
-
-    // parse root id
-    var parts = namespaceService.util.parseName(rootNodeId);
-
-    var isRooted = namespaceService.util.isRooted(rootNodeId);
-    var buildId = '';
-    if (!isRooted) { parts.unshift(''); } // create <Home> node
-
-    var parent; // used to connect each new node to their parent
-
-    parts.forEach(function(v) {
-      buildId += (buildId.length > 0 || isRooted ? '/' : '') + v;
-      var nn = rootIndex[buildId] || {};
-      var isNew = (nn.id === undefined);
-      nn.id = buildId;
-      nn.name = v || RELATIVE_ROOT;
-      nn.isLeaf = false;
-      nn.hasServer = true;  // assume true, fix later
-      nn.hasMountPoint = true;  // assume true, fix later
-      if (parent !== undefined) {
-        nn.parent = parent;
-        if (parent.children === undefined && parent._children === undefined) {
-          parent._children = [nn];  // initially hidden
-        }
-      }
-      if (isNew) {
-        rootIndex[buildId] = nn;
-      }
-      parent = nn;
-    });
-
-    root = parent;  // new root
-
-    if (selNode === undefined) {
-      selectNode(root);
+  D3Widget.prototype.init = function() {
+    if (!networkElem) {
+      networkElem = document.createElement('div');
+      networkElem.className = 'network';
+      networkElem.setAttribute('tabindex', 0); // allow focus
+      selectItem = this.browseEvents.selectItem.bind(this.browseEvents);
+      requestAnimationFrame(initD3);
     }
 
-    loadItem(root); // load rest of information for this node
-    loadSubItems(root); // load the children
-  }
+    // wrap in a new element, needed for Mercury vdom to patch properly.
+    var wrapper = document.createElement('div');
+    wrapper.className = 'networkParent';
+    wrapper.appendChild(networkElem);
 
-  updateD3(root, true); // always animate
-};
+    requestAnimationFrame(this.updateRoot.bind(this));
 
-// initialize d3 HTML elements
-function initD3() {
+    return wrapper;
+  };
 
-  // size of the diagram
-  width = networkElem.offsetWidth;
-  height = networkElem.offsetHeight;
+  // Keep track of previous namespace that was browsed to so we can
+  // know when navigating to a different namespace happens.
+  var previousNamespace;
 
-  // current pan, zoom, and rotation
-  curX = width / 2; // center
-  curY = height / 2;
-  curZ = modZ = 1.0; // current zoom
-  curR = 270; // current rotation
+  D3Widget.prototype.polyup = polyup;
+  D3Widget.prototype.polydown = polydown;
+  D3Widget.prototype.menu = menu;
+  D3Widget.prototype.update = function(browseState, browseEvents) {
 
-  // d3 diagonal projection for use by the node paths
-  diagonal= d3.svg.diagonal.radial().
+    // handle changed selection
+    selNode = rootIndex[browseState.selectedItemName] || selNode;
+
+    this.browseState = browseState;
+    this.browseEvents = browseEvents;
+
+    // check to see if window was resized while we were away
+    if (width !== networkElem.offsetWidth) {
+      resize();
+    }
+
+    this.updateRoot();
+
+    networkElem.focus();
+  };
+
+  // build new data tree
+  D3Widget.prototype.updateRoot = function() {
+    var rootNodeId = this.browseState.namespace;
+
+    if (previousNamespace !== rootNodeId) {
+
+      previousNamespace = rootNodeId;
+
+      // parse root id
+      var parts = namespaceService.util.parseName(rootNodeId);
+
+      var isRooted = namespaceService.util.isRooted(rootNodeId);
+      var buildId = '';
+      if (!isRooted) {
+        parts.unshift('');
+      } // create <Home> node
+
+      var parent; // used to connect each new node to their parent
+
+      parts.forEach(function(v) {
+        buildId += (buildId.length > 0 || isRooted ? '/' : '') + v;
+        var nn = rootIndex[buildId] || {};
+        var isNew = (nn.id === undefined);
+        nn.id = buildId;
+        nn.name = v || RELATIVE_ROOT;
+        nn.isLeaf = false;
+        nn.hasServer = true; // assume true, fix later
+        nn.hasMountPoint = true; // assume true, fix later
+        if (parent !== undefined) {
+          nn.parent = parent;
+          if (parent.children === undefined && parent._children === undefined) {
+            parent._children = [nn]; // initially hidden
+          }
+        }
+        if (isNew) {
+          rootIndex[buildId] = nn;
+        }
+        parent = nn;
+      });
+
+      root = parent; // new root
+
+      if (selNode === undefined) {
+        selectNode(root);
+      }
+
+      loadItem(root); // load rest of information for this node
+      loadSubItems(root); // load the children
+    }
+
+    updateD3(root, true); // always animate
+  };
+
+  // initialize d3 HTML elements
+  function initD3() {
+
+    // size of the diagram
+    width = networkElem.offsetWidth;
+    height = networkElem.offsetHeight;
+
+    // current pan, zoom, and rotation
+    curX = width / 2; // center
+    curY = height / 2;
+    curZ = modZ = 1.0; // current zoom
+    curR = 270; // current rotation
+
+    // d3 diagonal projection for use by the node paths
+    diagonal = d3.svg.diagonal.radial().
     projection(function(d) {
-        return [d.y, d.x / 180 * Math.PI];
+      return [d.y, d.x / 180 * Math.PI];
     });
 
-  // d3 tree layout
-  treeD3 = d3.layout.tree().
-    // circular coordinates to fit in window
-    // 120 is to allow space for text strings
+    // d3 tree layout
+    treeD3 = d3.layout.tree().
+      // circular coordinates to fit in window
+      // 120 is to allow space for text strings
     size([360, Math.min(width, height) / 2 - 120]).
-    // space between nodes, depends on if they have same parent
-    // dividing by a.depth is for radial coordinates
+      // space between nodes, depends on if they have same parent
+      // dividing by a.depth is for radial coordinates
     separation(function(a, b) {
-        return (a.parent === b.parent ? 1 : 2) / (a.depth + 1);
+      return (a.parent === b.parent ? 1 : 2) / (a.depth + 1);
     });
 
-  // define the svgBase, attaching a class for styling and the zoomListener
-  svgBase = d3.select('.network').append('svg').
+    // define the svgBase, attaching a class for styling and the zoomListener
+    svgBase = d3.select('.network').append('svg').
     attr('width', width).
     attr('height', height).
     attr('class', 'overlay').
     on('mousedown', mousedown);
 
-  // Group which holds all nodes and manages pan, zoom, rotate
-  svgGroup = svgBase.append('g').
+    // Group which holds all nodes and manages pan, zoom, rotate
+    svgGroup = svgBase.append('g').
     attr('transform', 'translate(' + curX + ',' + curY + ')');
 
-  networkElem.focus();
-  d3.select('.network'). // set up document events
-    on('wheel', wheel).  // zoom, rotate
+    networkElem.focus();
+    d3.select('.network'). // set up document events
+    on('wheel', wheel). // zoom, rotate
     on('keydown', keydown).
     on('keyup', keyup).
     on('mouseover', function() {
       networkElem.focus();
     });
-  d3.select(window).on('resize', resize);
-}
+    d3.select(window).on('resize', resize);
+  }
 
-// draw tree using d3js
-// subroot - source node of the update
-// doAni - whether to do a transition animation
-function updateD3(subroot, doAni) {
-  // length of d3 animation
-  var duration = (d3.event && d3.event.altKey ? DURATION * 4 : DURATION);
+  // draw tree using d3js
+  // subroot - source node of the update
+  // doAni - whether to do a transition animation
+  function updateD3(subroot, doAni) {
+      // length of d3 animation
+      var duration = (d3.event && d3.event.altKey ? DURATION * 4 : DURATION);
 
-  // Compute the new tree layout.
-  var d3nodes = treeD3.nodes(root);
-  var d3links = treeD3.links(d3nodes);
+      // Compute the new tree layout.
+      var d3nodes = treeD3.nodes(root);
+      var d3links = treeD3.links(d3nodes);
 
-  // Update the view
-  var view = doAni ? svgGroup.transition().duration(duration) : svgGroup;
+      // Update the view
+      var view = doAni ? svgGroup.transition().duration(duration) : svgGroup;
 
-  view.attr('transform',
+      view.attr('transform',
         'rotate(' + curR + ' ' + curX + ' ' + curY +
         ')translate(' + curX + ' ' + curY +
         ')scale(' + curZ + ')');
 
-  var gnode = svgGroup.selectAll('g.node').
-    data(d3nodes, function(d) {
+      var gnode = svgGroup.selectAll('g.node').
+      data(d3nodes, function(d) {
         return d.id;
-  });
+      });
 
-  // Enter any new nodes at the parent's previous position
-  var nodeEnter = gnode.enter().insert('g', ':first-child').
-    attr('class', 'node').
-    attr('opacity', 0).
-    attr('transform', 'rotate(' + (subroot.x - 90) +
+      // Enter any new nodes at the parent's previous position
+      var nodeEnter = gnode.enter().insert('g', ':first-child').
+      attr('class', 'node').
+      attr('opacity', 0).
+      attr('transform', 'rotate(' + (subroot.x - 90) +
         ')translate(' + subroot.y + ')').
-    on('click', click).on('dblclick', dblclick).
-    on('contextmenu', showContextMenu);
+      on('click', click).on('dblclick', dblclick).
+      on('contextmenu', showContextMenu);
 
-  nodeEnter.append('title').text(function(d) {
-      return getServiceIcon(d).title;
-  });
+      nodeEnter.append('title').text(function(d) {
+        return getServiceIcon(d).title;
+      });
 
-  nodeEnter.filter(function(d) {  // Mount Point
-      return d.hasMountPoint;
-    }).append('path').attr('class', 'mountpointicon').
-    attr('d', d3.svg.symbol().type('square'));
+      nodeEnter.filter(function(d) { // Mount Point
+        return d.hasMountPoint;
+      }).append('path').attr('class', 'mountpointicon').
+      attr('d', d3.svg.symbol().type('square'));
 
-  nodeEnter.filter(function(d) {  // Server
-      return d.hasServer;
-    }).append('path').attr('class', 'servericon').
-    attr('d', d3.svg.symbol().type('circle').size(60));
+      nodeEnter.filter(function(d) { // Server
+        return d.hasServer;
+      }).append('path').attr('class', 'servericon').
+      attr('d', d3.svg.symbol().type('circle').size(60));
 
-  nodeEnter.append('text').
-    text(function(d) { return d.name; }).
-    attr('transform', ((subroot.x + curR) % 360 <= 180 ?
-            'rotate(-7)translate(8)scale(' : 'rotate(187)translate(-8)scale('
-          ) + modZ + ')' );
+      nodeEnter.append('text').
+      text(function(d) {
+        return d.name;
+      }).
+      attr('transform', ((subroot.x + curR) % 360 <= 180 ?
+        'rotate(-7)translate(8)scale(' : 'rotate(187)translate(-8)scale('
+      ) + modZ + ')');
 
-  // update existing graph nodes
+      // update existing graph nodes
 
-  // set path style for selection, loading, and scale
-  gnode.select('path').
-    attr('transform', 'scale(' + modZ + ')').
-    classed('loading', function(d) { return d.loading; }).
-    // style('fill', function(d) {
-    //   // only show children_color if the children are not shown
-    //   return d.isLeaf || d.children ? NO_CHILDREN_COLOR : HAS_CHILDREN_COLOR;
-    // }).
-    attr('stroke', function(d) {
+      // set path style for selection, loading, and scale
+      gnode.select('path').
+      attr('transform', 'scale(' + modZ + ')').
+      classed('loading', function(d) {
+        return d.loading;
+      }).
+      attr('stroke', function(d) {
         return d === selNode ? SELECTED_COLOR : SYMBOL_STROKE_COLOR;
-    }).
-    attr('stroke-width', function(d) {
+      }).
+      attr('stroke-width', function(d) {
         return d === selNode ? 3 : 2;
-    });
+      });
 
-  gnode.select('title').text(function(d) {
-      return getServiceIcon(d).title;
-  });
+      gnode.select('title').text(function(d) {
+        return getServiceIcon(d).title;
+      });
 
-  // remove icons if assumed true but turn out to be false
-  gnode.select('path.servericon').filter(function(d) {
-    return !d.hasServer;
-  }).remove();
-  gnode.select('path.mountpointicon').filter(function(d) {
-    return !d.hasMountPoint;
-  }).remove();
+      // remove icons if assumed true but turn out to be false
+      gnode.select('path.servericon').filter(function(d) {
+        return !d.hasServer;
+      }).remove();
+      gnode.select('path.mountpointicon').filter(function(d) {
+        return !d.hasMountPoint;
+      }).remove();
 
-  gnode.select('text').
-    attr('text-anchor', function(d) {
+      gnode.select('text').
+      attr('text-anchor', function(d) {
         return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
-    }).
-    attr('transform', function(d) {
-      return ((d.x + curR) % 360 <= 180 ?
+      }).
+      attr('transform', function(d) {
+        return ((d.x + curR) % 360 <= 180 ?
           'rotate(-7)translate(8)scale(' :
           'rotate(187)translate(-8)scale('
-        ) + modZ +')';
-    }).
-    attr('fill', function(d) {
+        ) + modZ + ')';
+      }).
+      attr('fill', function(d) {
         return d === selNode ? SELECTED_COLOR : 'black';
-    }).
-    attr('dy', '5px');
+      }).
+      attr('dy', '5px');
 
-  var nodeUpdate = (doAni ?
-      gnode.transition().duration(duration).
+      var nodeUpdate = (doAni ?
+        gnode.transition().duration(duration).delay(function(d, i) {
+          return i * STAGGER + Math.max(0, d.depth - selNode.depth);
+        }) : gnode);
+
+      nodeUpdate.attr('transform', function(d) {
+        return 'rotate(' + (d.x - 90) + ')translate(' + d.y + ')';
+      }).style('opacity', 1);
+
+      nodeUpdate.select('path').
+      attr('transform', 'scale(' + modZ + ')');
+
+      // Transition exiting nodes to the parent's new position and remove
+      var nodeExit = doAni ? gnode.exit().transition().duration(duration).
       delay(function(d, i) {
-        return i * STAGGER + Math.max(0, d.depth - selNode.depth);
-      }) : gnode);
+        return i * STAGGER;
+      }): gnode.exit();
 
-  nodeUpdate.attr('transform', function(d) {
-      return 'rotate(' + (d.x - 90) + ')translate(' + d.y + ')';
-    }).style('opacity', 1);
+      nodeExit.attr('transform', function(d) {
+        return 'rotate(' + (subroot.x - 90) + ')translate(' + subroot.y + ')';
+      }).
+      attr('opacity', 0).
+      remove();
 
-  nodeUpdate.select('path').
-    attr('transform', 'scale(' + modZ + ')');
+      nodeExit.select('.node path').attr('transform', 'scale(0)');
+      nodeExit.select('.node text').style('fill-opacity', 0);
 
-  // Transition exiting nodes to the parent's new position and remove
-  var nodeExit = doAni ? gnode.exit().transition().duration(duration).
-    delay(function(d, i) { return i * STAGGER; }) : gnode.exit();
+      // Update the links…
+      var glink = svgGroup.selectAll('path.link').
+      data(d3links, function(d) {
+        return d.target.id;
+      });
 
-  nodeExit.attr('transform', function(d) {
-        return 'rotate(' + (subroot.x - 90) +')translate(' + subroot.y + ')';
-    }).
-    attr('opacity', 0).
-    remove();
-
-  nodeExit.select('.node path').attr('transform', 'scale(0)');
-  nodeExit.select('.node text').style('fill-opacity', 0);
-
-  // Update the links…
-  var glink = svgGroup.selectAll('path.link').
-    data(d3links, function(d) {
-      return d.target.id;
-    });
-
-  // Enter any new links at the parent's previous position
-  glink.enter().insert('path', 'g').
-    attr('class', 'link').
-    attr('d', function() {
+      // Enter any new links at the parent's previous position
+      glink.enter().insert('path', 'g').
+      attr('class', 'link').
+      attr('d', function() {
         var o = {
           x: subroot.x,
           y: subroot.y
@@ -459,18 +472,17 @@
           source: o,
           target: o
         });
-    });
+      });
 
-  // Transition links to their new position
-  (doAni ? glink.transition().duration(duration).
-    delay(function(d, i) {
-      return i * STAGGER + Math.max(0, d.source.depth - selNode.depth);
-    }) : glink).
-    attr('d', diagonal);
+      // Transition links to their new position
+      (doAni ? glink.transition().duration(duration).delay(function(d, i) {
+        return i * STAGGER + Math.max(0, d.source.depth - selNode.depth);
+      }) : glink).
+      attr('d', diagonal);
 
-  // Transition exiting nodes to the parent's new position
-  (doAni ? glink.exit().transition().duration(duration) : glink.exit()).
-    attr('d', function() {
+      // Transition exiting nodes to the parent's new position
+      (doAni ? glink.exit().transition().duration(duration) : glink.exit()).
+      attr('d', function() {
         var o = {
           x: subroot.x,
           y: subroot.y
@@ -479,518 +491,559 @@
           source: o,
           target: o
         });
-    }).
-    remove(); // remove edge at end of animation
-} // end updateD3
+      }).
+      remove(); // remove edge at end of animation
+    } // end updateD3
 
-// find place to insert new node in children
-var bisectfun = d3.bisector(function(d) { return d.name; }).right;
+  // find place to insert new node in children
+  var bisectfun = d3.bisector(function(d) {
+    return d.name;
+  }).right;
 
-// create node or merge new item data into it
-function mergeNode(item, parent) {
-  var nn = rootIndex[item.objectName] || {};
-  var isNew = nn.id === undefined; // not found in rootIndex
-  nn.id = item.objectName;
-  nn.name = item.mountedName || RELATIVE_ROOT;
-  nn.parent = parent || nn.parent;
-  nn.isLeaf = item.isLeaf;
-  nn.hasMountPoint = item.hasMountPoint;
-  nn.hasServer = item.hasServer;
-  if (isNew && parent !== undefined) { // insert node in proper place
-    rootIndex[nn.id] = nn;
-    if (parent.children === undefined) {
-      parent.children = [nn];
+  // create node or merge new item data into it
+  function mergeNode(item, parent) {
+      var nn = rootIndex[item.objectName] || {};
+      var isNew = nn.id === undefined; // not found in rootIndex
+      nn.id = item.objectName;
+      nn.name = item.mountedName || RELATIVE_ROOT;
+      nn.parent = parent || nn.parent;
+      nn.isLeaf = item.isLeaf;
+      nn.hasMountPoint = item.hasMountPoint;
+      nn.hasServer = item.hasServer;
+      if (isNew && parent !== undefined) { // insert node in proper place
+        rootIndex[nn.id] = nn;
+        if (parent.children === undefined) {
+          parent.children = [nn];
+        } else {
+          parent.children.splice(bisectfun(parent.children, nn.name), 0, nn);
+        }
+      }
+      return isNew; // need to animate it in
+    } // end mergeNode
+
+  function loadItem(node) { // load a single item (used for root of tree)
+    namespaceService.getNamespaceItem(node.id).then(function(observable) {
+      mercury.watch(observable, updateItem);
+
+      function updateItem(item) { // currently only gets called once
+        mergeNode(item, node.parent); // update elsewhere
+      }
+    });
+  }
+
+  // load children items asynchronously
+  function loadSubItems(node) {
+      if (node.subNodesLoaded) {
+        return;
+      }
+      var namespace = node.id;
+      if (node._children) {
+        node.children = node._children;
+        node._children = null;
+      }
+      node.subNodesLoaded = true;
+      showLoading(node, true); // node is loading
+
+      namespaceService.getChildren(namespace).then(function(resultObservable) {
+        var initialValues = resultObservable();
+        initialValues.forEach(function(item) {
+          batchUpdate(node, mergeNode(item, node));
+        });
+        resultObservable.events.once('end', function() {
+          showLoading(node, false); // node no longer loading
+        });
+
+        resultObservable(updatedValues);
+
+        function updatedValues(results) {
+          // TODO(wmleler) support removed and updated nodes for watchGlob
+          var item = results._diff[0][2]; // changed item from Mercury
+          batchUpdate(node, mergeNode(item, node));
+        }
+      }).catch(function(err) {
+        log.error('glob failed', err);
+      });
+    } // end loadSubItems
+
+  // batch up groups of updates to speed up transitions
+  var batchNode = null;
+  var batchId = null;
+
+  function batchUpdate(node, doAni) {
+    if (node !== batchNode) {
+      if (batchNode !== null) {
+        updateD3(batchNode, doAni);
+      }
+      batchNode = node;
+      if (batchId !== null) {
+        clearTimeout(batchId);
+        batchId = null;
+      }
     } else {
-      parent.children.splice(bisectfun(parent.children, nn.name), 0, nn);
+      batchId = setTimeout(function() {
+        updateD3(batchNode, doAni);
+        batchId = null;
+      }, DURATION);
     }
   }
-  return isNew; // need to animate it in
-} // end mergeNode
 
-function loadItem(node) { // load a single item (used for root of tree)
-  namespaceService.getNamespaceItem(node.id).then(function(observable) {
-    mercury.watch(observable, updateItem);
-
-    function updateItem(item) { // currently only gets called once (no updates)
-      mergeNode(item, node.parent); // update elsewhere
+  function selectNode(node) { // highlight node and show details
+    if (node === selNode) {
+      return;
     }
-  });
-}
-
-// load children items asynchronously
-function loadSubItems(node) {
-  if (node.subNodesLoaded) { return; }
-  var namespace = node.id;
-  if (node._children) {
-    node.children = node._children;
-    node._children = null;
+    selNode = node;
+    selectItem({
+      name: node.id
+    }); // notify rest of app
   }
-  node.subNodesLoaded = true;
-  showLoading(node, true);  // node is loading
 
-  namespaceService.getChildren(namespace).then(function(resultObservable) {
-    var initialValues = resultObservable();
-    initialValues.forEach(function(item) {
-        batchUpdate(node, mergeNode(item, node));
+  function browseInto(node) { // make this node the root
+    var browseUrl = browseRoute.createUrl(browseInto.browseState, {
+      namespace: node.id
     });
-    resultObservable.events.once('end', function() {
-      showLoading(node, false); // node no longer loading
+    browseInto.navEvents.navigate({
+      path: browseUrl
     });
-
-    resultObservable(updatedValues);
-
-    function updatedValues(results) {
-      // TODO(wmleler) support removed and updated nodes for watchGlob
-      var item = results._diff[0][2]; // changed item from Mercury
-      batchUpdate(node, mergeNode(item, node));
-    }
-  }).catch(function(err) {
-    log.error('glob failed', err);
-  });
-} // end loadSubItems
-
-// batch up groups of updates to speed up transitions
-var batchNode = null;
-var batchId = null;
-
-function batchUpdate(node, doAni) {
-  if (node !== batchNode) {
-    if (batchNode !== null) { updateD3(batchNode, doAni); }
-    batchNode = node;
-    if (batchId !== null) {
-      clearTimeout(batchId);
-      batchId = null;
-    }
-  } else {
-    batchId = setTimeout(function() {
-      updateD3(batchNode, doAni);
-      batchId = null;
-    }, DURATION);
   }
-}
 
-function selectNode(node) { // highlight node and show details
-  if (node === selNode) { return; }
-  selNode = node;
-  selectItem({ name: node.id });  // notify rest of app
-}
-
-function browseInto(node) { // make this node the root
-  var browseUrl = browseRoute.createUrl(browseInto.browseState, {
-    namespace: node.id
-  });
-  browseInto.navEvents.navigate({ path: browseUrl });
-}
-
-// set view with no animation
-function setview() {
-  svgGroup.attr('transform',
+  // set view with no animation
+  function setview() {
+    svgGroup.attr('transform',
       'rotate(' + curR + ' ' + curX + ' ' + curY +
       ')translate(' + curX + ' ' + curY +
       ')scale(' + curZ + ')');
-  svgGroup.selectAll('text').
-      attr('text-anchor', function(d) {
-          return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
-      }).
-      attr('transform', function(d) {
-          return ((d.x + curR) % 360 <= 180 ?
-              'rotate(-7)translate(8)scale(' :
-              'rotate(187)translate(-8)scale('
-            ) + modZ +')';
-      });
-  svgGroup.selectAll('.node path').
+    svgGroup.selectAll('text').
+    attr('text-anchor', function(d) {
+      return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
+    }).
+    attr('transform', function(d) {
+      return ((d.x + curR) % 360 <= 180 ?
+        'rotate(-7)translate(8)scale(' :
+        'rotate(187)translate(-8)scale('
+      ) + modZ + ')';
+    });
+    svgGroup.selectAll('.node path').
     attr('transform', 'scale(' + modZ + ')').
     attr('stroke-width', function(d) {
-        return d === selNode ? 3 : 2;
+      return d === selNode ? 3 : 2;
     });
-}
-
-// show nodes that are loading
-function showLoading(node, v) {
-  node.loading = v;
-  svgGroup.selectAll('.node path').
-    classed('loading', function(d) { return d.loading; });
-}
-
-//
-// Helper functions for collapsing and expanding nodes
-//
-
-// Toggle expand / collapse
-function toggle(d) {
-  if (d.children) {
-    d._children = d.children;
-    d.children = null;
-  } else if (d._children && d.subNodesLoaded) {
-    d.children = d._children;
-    d._children = null;
-  } else {
-    loadSubItems(d);
   }
-}
 
-function collapse(d) {  // collapse one level
-  if (d.children) {
-    d._children = d.children;
-    d.children = null;
+  // show nodes that are loading
+  function showLoading(node, v) {
+    node.loading = v;
+    svgGroup.selectAll('.node path').
+    classed('loading', function(d) {
+      return d.loading;
+    });
   }
-}
 
-// expand all loaded children and descendents
-function expandTree(d) {
-  if (d._children) {
-    d.children = d._children;
-    d._children = null;
-  }
-  if (d.children) {
-    d.children.forEach(expandTree);
-  }
-}
+  //
+  // Helper functions for collapsing and expanding nodes
+  //
 
-// expand one level of tree using breadth first search
-function expand1Level(d) {
-  var q = [d]; // non-recursive using queue
-  var cn;
-  var done = null;
-  while (q.length > 0) {
-    cn = q.shift();
-    if (done !== null && done < cn.depth) { return; }
-    if (cn._children) {
-      done = cn.depth;
-      cn.children = cn._children;
-      cn._children = null;
-      cn.children.forEach(collapse);
-    } else if (!(cn.isLeaf || cn.subNodesLoaded)) {
-      done = cn.depth;
-      loadSubItems(cn);
-    }
-    if (cn.children) {
-      q = q.concat(cn.children);
+  // Toggle expand / collapse
+  function toggle(d) {
+    if (d.children) {
+      d._children = d.children;
+      d.children = null;
+    } else if (d._children && d.subNodesLoaded) {
+      d.children = d._children;
+      d._children = null;
+    } else {
+      loadSubItems(d);
     }
   }
-  // no nodes to open
-}
 
-var moveX = 0, moveY = 0, moveZ = 0, moveR = 0; // animations
-var keysdown = [];  // which keys are currently down
-var animation = null;
-var aniTime = null; // time since last animation frame
-
-// update animation frame
-function frame(frametime) {
-  var diff = aniTime ? (frametime - aniTime) / 16 : 0;
-  aniTime = frametime;
-
-  var dz = Math.pow(1.2, diff * moveZ);
-  var newZ = limitZ(curZ * dz);
-  dz = newZ / curZ;
-  curZ = newZ;
-  modZ = Math.pow(1.1, -curZ);  // limit text and node size as scale increases
-  curX += diff * moveX - (width / 2- curX) * (dz - 1);
-  curY += diff * moveY - (height / 2 - curY) * (dz - 1);
-  curR = limitR(curR + diff * moveR);
-  setview();
-  animation = requestAnimationFrame(frame);
-}
-
-// enforce zoom extent
-function limitZ(z) {
-  return Math.max(Math.min(z, MAX_ZOOM), MIN_ZOOM);
-}
-
-// keep rotation between 0 and 360
-function limitR(r) {
-  return (r + 360) % 360;
-}
-
-//
-// d3 event handlers
-//
-
-function resize() { // window resize
-  if (networkElem.offsetWidth === 0) { return; }
-  var oldwidth = width;
-  var oldheight = height;
-  width = networkElem.offsetWidth;
-  height = networkElem.offsetHeight;
-  treeD3.size([360, Math.min(width, height) / 2 - 120]);
-  svgBase.attr('width', width).attr('height', height);
-  curX += (width - oldwidth) / 2;
-  curY += (height - oldheight) / 2;
-  svgGroup.attr('transform',
-    'rotate(' + curR + ' ' + curX + ' ' + curY +
-    ')translate(' + curX + ' ' + curY +
-    ')scale(' + curZ + ')');
-  updateD3(root, false);
-}
-
-function click(d) { // Select node
-  if (d3.event.defaultPrevented || d === selNode) { return; }
-  selectNode(d);
-  updateD3(d, false);
-  d3.event.preventDefault();
-}
-
-function dblclick(d) {  // Toggle children of node
-  if (d3.event.defaultPrevented) { return; } // click suppressed
-  d3.event.preventDefault();
-  if (d3.event.shiftKey) {
-    expand1Level(d);
-  } else {
-    toggle(d);
+  function collapse(d) { // collapse one level
+    if (d.children) {
+      d._children = d.children;
+      d.children = null;
+    }
   }
-  updateD3(d, true);
-}
 
-var startposX, startposY; // initial position on mouse button down for pan
+  // expand all loaded children and descendents
+  function expandTree(d) {
+    if (d._children) {
+      d.children = d._children;
+      d._children = null;
+    }
+    if (d.children) {
+      d.children.forEach(expandTree);
+    }
+  }
 
-function mousedown() {  // pan action from mouse drag
-  if (d3.event.which !== 1) { return; } // ingore other mouse buttons
-  startposX = curX - d3.event.clientX;
-  startposY = curY - d3.event.clientY;
-  d3.select(document).on('mousemove', mousemove, true);
-  d3.select(document).on('mouseup', mouseup, true);
-  networkElem.focus();
-  d3.event.preventDefault();
-}
+  // expand one level of tree using breadth first search
+  function expand1Level(d) {
+    var q = [d]; // non-recursive using queue
+    var cn;
+    var done = null;
+    while (q.length > 0) {
+      cn = q.shift();
+      if (done !== null && done < cn.depth) {
+        return;
+      }
+      if (cn._children) {
+        done = cn.depth;
+        cn.children = cn._children;
+        cn._children = null;
+        cn.children.forEach(collapse);
+      } else if (!(cn.isLeaf || cn.subNodesLoaded)) {
+        done = cn.depth;
+        loadSubItems(cn);
+      }
+      if (cn.children) {
+        q = q.concat(cn.children);
+      }
+    }
+    // no nodes to open
+  }
 
-function mousemove() {  // drag
-  curX = startposX + d3.event.clientX;
-  curY = startposY + d3.event.clientY;
-  setview();
-  d3.event.preventDefault();
-}
+  var moveX = 0,
+    moveY = 0,
+    moveZ = 0,
+    moveR = 0; // animations
+  var keysdown = []; // which keys are currently down
+  var animation = null;
+  var aniTime = null; // time since last animation frame
 
-function mouseup() {  // cleanup
-  d3.select(document).on('mousemove', null);
-  d3.select(document).on('mouseup', null);
-}
+  // update animation frame
+  function frame(frametime) {
+    var diff = aniTime ? (frametime - aniTime) / 16 : 0;
+    aniTime = frametime;
 
-function wheel() {  // mousewheel (including left-right)
-  var dz, newZ;
-  var slow = (d3.event && d3.event.altKey) ? 0.25 : 1;
-  if (d3.event.wheelDeltaY !== 0) {  // up-down = zoom
-    dz = Math.pow(1.2, d3.event.wheelDeltaY * 0.001 * slow);
-    newZ = limitZ(curZ * dz);
+    var dz = Math.pow(1.2, diff * moveZ);
+    var newZ = limitZ(curZ * dz);
     dz = newZ / curZ;
     curZ = newZ;
-    // zoom around mouse position
-    curX -= (d3.event.clientX - curX) * (dz - 1);
-    curY -= (d3.event.clientY - curY) * (dz - 1);
+    modZ = Math.pow(1.1, -curZ); // limit text and node size as scale increases
+    curX += diff * moveX - (width / 2 - curX) * (dz - 1);
+    curY += diff * moveY - (height / 2 - curY) * (dz - 1);
+    curR = limitR(curR + diff * moveR);
     setview();
-  }
-  if (d3.event.wheelDeltaX !== 0) {  // left-right = rotate
-    curR = limitR(curR + d3.event.wheelDeltaX * 0.01 * slow);
-    updateD3(root, false);
-  }
-}
-
-function polydown(key, evt) { // polymer ev-mousedown event
-  actionDown(key, evt.shiftKey, evt.altKey);
-}
-
-function polyup(key, evt) { // polymer ev-mouseup event
-  actionUp(key);
-}
-
-function menu(key, shift, evt) { // context menu selection event
-  if (evt === undefined) {  // shiftkey not supplied
-    evt = shift;
-    shift = evt.shiftKey;
-  }
-  actionDown(key, shift, evt.altKey);
-  networkElem.focus();
-}
-
-function keydown() {  // d3 keydown event
-  var evt = d3.event;
-  if (evt.repeat) { return; }
-  actionDown(evt.which, evt.shiftKey, evt.altKey);
-}
-
-function keyup() {  // d3 keyup event
-  var evt = d3.event;
-  actionUp(evt.which);
-}
-
-// right click, show context menu and select this node
-function showContextMenu(d) {
-  d3.event.preventDefault();
-  d3.select('.ecnode').text(
-      (d.children ? 'Collapse ' : 'Expand ') + 'Node' );
-  var cmenu = d3.select('.contextmenu');
-  cmenu.style({
-    left: Math.min(d3.event.offsetX + 3,
-        width - cmenu.style('width').replace('px', '') - 5) + 'px',
-    top: (d3.event.offsetY + 8) + 'px',
-    display: 'block'
-  });
-  var doc = d3.select(document);
-  doc.on('mousedown.cm', hideContextMenu, true);
-  setTimeout(function() {
-    doc.on('mouseup.cm', hideContextMenu, true);
-  }, 500);
-  selectNode(d);
-}
-
-function hideContextMenu() {
-  var doc = d3.select(document);
-  d3.select('.contextmenu').style('display', 'none');
-  doc.on('mouseup.cm', null);
-  doc.on('mousedown.cm', null);
-  networkElem.focus();
-}
-
-
-// Event actions
-// Almost all UI actions pass through here,
-// even if they are not originally generated from the keyboard
-// There are two types of actions:
-// * Press-and-Hold actions perform some action while they are pressed,
-//   until they are released, like pan, zoom, and rotate. These actions end
-//   with "break", so the key can be saved, and actionUp can stop the action.
-// * Click actions mostly happen on keydown, like toggling children.
-function actionDown(key, shift, alt) {
-  var parch; // parent's children
-  var slow = alt ? 0.25 : 1;
-  if (keysdown.indexOf(key) >= 0) { return; } // defeat auto repeat
-  switch(key) {
-    case KEY_PLUS: // zoom in
-      moveZ = ZOOM_INC * slow;
-      break;
-    case KEY_MINUS: // zoom out
-      moveZ = -ZOOM_INC * slow;
-      break;
-    case KEY_PAGEUP: // rotate counterclockwise
-      moveR = -ROT_INC * slow;
-      break;
-    case KEY_PAGEDOWN: // rotate clockwise
-      moveR = ROT_INC * slow;
-      break;
-    case KEY_LEFT:
-      if (shift) { // move selection to parent
-        if (!selNode) {
-          selectNode(root);
-        } else if (selNode.parent) {
-          selectNode(selNode.parent);
-          updateD3(selNode, false);
-        }
-        return;
-      }
-      moveX = -PAN_INC * slow;  // pan left
-      break;
-    case KEY_UP:
-      if (shift) { // move selection to previous child
-        if (!selNode) {
-          selectNode(root);
-        } else if (selNode.parent) {
-          parch = selNode.parent.children;
-          selectNode(parch[(parch.indexOf(selNode) +
-              parch.length - 1) % parch.length]);
-          updateD3(selNode, false);
-        }
-        return;
-      }
-      moveY = -PAN_INC * slow;  // pan up
-      break;
-    case KEY_RIGHT:
-      if (shift) { // move selection to first/last child
-        if (!selNode) {
-          selectNode(root);
-        } else {
-          if (selNode.children && selNode.children.length > 0) {
-            selectNode(selNode.children[0]);
-            updateD3(selNode, false);
-          }
-        }
-        return;
-      }
-      moveX = PAN_INC * slow; // pan right
-      break;
-    case KEY_DOWN:
-      if (shift) { // move selection to next child
-        if (!selNode) {
-          selectNode(root);
-        } else if (selNode.parent) {
-          parch = selNode.parent.children;
-          selectNode(parch[(parch.indexOf(selNode) + 1) % parch.length]);
-          updateD3(selNode, false);
-        }
-        return;
-      }
-      moveY = PAN_INC * slow;  // pan down
-      break;
-    case KEY_RETURN:
-      if (!selNode) {
-        selectNode(root);
-      }
-      if (shift) {  // show loaded
-        expandTree(selNode);
-        loadSubItems(selNode);
-      } else {
-        toggle(selNode);  // expand/collapse node
-      }
-      updateD3(selNode, true);
-      return;
-    case KEY_SPACE:
-      if (!selNode) {
-        selectNode(root);
-      }
-      if (shift) { // browse into
-        browseInto(selNode);
-      } else { // load +1 level
-        expand1Level(selNode);
-        updateD3(selNode, true);
-      }
-      return;
-    case KEY_HOME: // reset transform
-      curX = width / 2;
-      curY = height / 2;
-      curR = limitR(90 - root.x);
-      curZ = 1;
-      updateD3(root, true);
-      return;
-    case KEY_END: // zoom to selection
-      if (!selNode) { return; }
-      curX = width / 2 - selNode.y * curZ;
-      curY = height / 2;
-      curR = limitR(90 - selNode.x);
-      updateD3(selNode, true);
-      return;
-    default: return;  // ignore other keys
-  }
-  keysdown.push(key);
-  // start animation if anything happening
-  if (keysdown.length > 0 && animation === null) {
     animation = requestAnimationFrame(frame);
   }
-}
 
-function actionUp(key) {
-  var pos = keysdown.indexOf(key);
-  if (pos < 0) { return; }
-
-  switch(key) {
-    case KEY_PLUS: // - = zoom out
-    case KEY_MINUS: // + = zoom in
-      moveZ = 0;
-      break;
-    case KEY_PAGEUP: // page up = rotate CCW
-    case KEY_PAGEDOWN: // page down = rotate CW
-      moveR = 0;
-      break;
-    case KEY_LEFT: // left arrow
-    case KEY_RIGHT: // right arrow
-      moveX = 0;
-      break;
-    case KEY_UP: // up arrow
-    case KEY_DOWN: // down arrow
-      moveY = 0;
-      break;
+  // enforce zoom extent
+  function limitZ(z) {
+    return Math.max(Math.min(z, MAX_ZOOM), MIN_ZOOM);
   }
-  keysdown.splice(pos, 1);  // remove key
-  if (keysdown.length > 0 || animation === null) { return; }
-  cancelAnimationFrame(animation);
-  animation = aniTime = null;
-  networkElem.focus();
-}
+
+  // keep rotation between 0 and 360
+  function limitR(r) {
+    return (r + 360) % 360;
+  }
+
+  //
+  // d3 event handlers
+  //
+
+  function resize() { // window resize
+    if (networkElem.offsetWidth === 0) {
+      return;
+    }
+    var oldwidth = width;
+    var oldheight = height;
+    width = networkElem.offsetWidth;
+    height = networkElem.offsetHeight;
+    treeD3.size([360, Math.min(width, height) / 2 - 120]);
+    svgBase.attr('width', width).attr('height', height);
+    curX += (width - oldwidth) / 2;
+    curY += (height - oldheight) / 2;
+    svgGroup.attr('transform',
+      'rotate(' + curR + ' ' + curX + ' ' + curY +
+      ')translate(' + curX + ' ' + curY +
+      ')scale(' + curZ + ')');
+    updateD3(root, false);
+  }
+
+  function click(d) { // Select node
+    if (d3.event.defaultPrevented || d === selNode) {
+      return;
+    }
+    selectNode(d);
+    updateD3(d, false);
+    d3.event.preventDefault();
+  }
+
+  function dblclick(d) { // Toggle children of node
+    if (d3.event.defaultPrevented) {
+      return;
+    } // click suppressed
+    d3.event.preventDefault();
+    if (d3.event.shiftKey) {
+      expand1Level(d);
+    } else {
+      toggle(d);
+    }
+    updateD3(d, true);
+  }
+
+  var startposX, startposY; // initial position on mouse button down for pan
+
+  function mousedown() { // pan action from mouse drag
+    if (d3.event.which !== 1) {
+      return;
+    } // ingore other mouse buttons
+    startposX = curX - d3.event.clientX;
+    startposY = curY - d3.event.clientY;
+    d3.select(document).on('mousemove', mousemove, true);
+    d3.select(document).on('mouseup', mouseup, true);
+    networkElem.focus();
+    d3.event.preventDefault();
+  }
+
+  function mousemove() { // drag
+    curX = startposX + d3.event.clientX;
+    curY = startposY + d3.event.clientY;
+    setview();
+    d3.event.preventDefault();
+  }
+
+  function mouseup() { // cleanup
+    d3.select(document).on('mousemove', null);
+    d3.select(document).on('mouseup', null);
+  }
+
+  function wheel() { // mousewheel (including left-right)
+    var dz, newZ;
+    var slow = (d3.event && d3.event.altKey) ? 0.25 : 1;
+    if (d3.event.wheelDeltaY !== 0) { // up-down = zoom
+      dz = Math.pow(1.2, d3.event.wheelDeltaY * 0.001 * slow);
+      newZ = limitZ(curZ * dz);
+      dz = newZ / curZ;
+      curZ = newZ;
+      // zoom around mouse position
+      curX -= (d3.event.clientX - curX) * (dz - 1);
+      curY -= (d3.event.clientY - curY) * (dz - 1);
+      setview();
+    }
+    if (d3.event.wheelDeltaX !== 0) { // left-right = rotate
+      curR = limitR(curR + d3.event.wheelDeltaX * 0.01 * slow);
+      updateD3(root, false);
+    }
+  }
+
+  function polydown(key, evt) { // polymer ev-mousedown event
+    actionDown(key, evt.shiftKey, evt.altKey);
+  }
+
+  function polyup(key, evt) { // polymer ev-mouseup event
+    actionUp(key);
+  }
+
+  function menu(key, shift, evt) { // context menu selection event
+    if (evt === undefined) { // shiftkey not supplied
+      evt = shift;
+      shift = evt.shiftKey;
+    }
+    actionDown(key, shift, evt.altKey);
+    networkElem.focus();
+  }
+
+  function keydown() { // d3 keydown event
+    var evt = d3.event;
+    if (evt.repeat) {
+      return;
+    }
+    actionDown(evt.which, evt.shiftKey, evt.altKey);
+  }
+
+  function keyup() { // d3 keyup event
+    var evt = d3.event;
+    actionUp(evt.which);
+  }
+
+  // right click, show context menu and select this node
+  function showContextMenu(d) {
+    d3.event.preventDefault();
+    d3.select('.ecnode').text(
+      (d.children ? 'Collapse ' : 'Expand ') + 'Node');
+    var cmenu = d3.select('.contextmenu');
+    cmenu.style({
+      left: Math.min(d3.event.offsetX + 3,
+        width - cmenu.style('width').replace('px', '') - 5) + 'px',
+      top: (d3.event.offsetY + 8) + 'px',
+      display: 'block'
+    });
+    var doc = d3.select(document);
+    doc.on('mousedown.cm', hideContextMenu, true);
+    setTimeout(function() {
+      doc.on('mouseup.cm', hideContextMenu, true);
+    }, 500);
+    selectNode(d);
+  }
+
+  function hideContextMenu() {
+    var doc = d3.select(document);
+    d3.select('.contextmenu').style('display', 'none');
+    doc.on('mouseup.cm', null);
+    doc.on('mousedown.cm', null);
+    networkElem.focus();
+  }
+
+
+  // Event actions
+  // Almost all UI actions pass through here,
+  // even if they are not originally generated from the keyboard
+  // There are two types of actions:
+  // * Press-and-Hold actions perform some action while they are pressed,
+  //   until they are released, like pan, zoom, and rotate. These actions end
+  //   with "break", so the key can be saved, and actionUp can stop the action.
+  // * Click actions mostly happen on keydown, like toggling children.
+  function actionDown(key, shift, alt) {
+    var parch; // parent's children
+    var slow = alt ? 0.25 : 1;
+    if (keysdown.indexOf(key) >= 0) {
+      return;
+    } // defeat auto repeat
+    switch (key) {
+      case KEY_PLUS: // zoom in
+        moveZ = ZOOM_INC * slow;
+        break;
+      case KEY_MINUS: // zoom out
+        moveZ = -ZOOM_INC * slow;
+        break;
+      case KEY_PAGEUP: // rotate counterclockwise
+        moveR = -ROT_INC * slow;
+        break;
+      case KEY_PAGEDOWN: // rotate clockwise
+        moveR = ROT_INC * slow;
+        break;
+      case KEY_LEFT:
+        if (shift) { // move selection to parent
+          if (!selNode) {
+            selectNode(root);
+          } else if (selNode.parent) {
+            selectNode(selNode.parent);
+            updateD3(selNode, false);
+          }
+          return;
+        }
+        moveX = -PAN_INC * slow; // pan left
+        break;
+      case KEY_UP:
+        if (shift) { // move selection to previous child
+          if (!selNode) {
+            selectNode(root);
+          } else if (selNode.parent) {
+            parch = selNode.parent.children;
+            selectNode(parch[(parch.indexOf(selNode) +
+              parch.length - 1) % parch.length]);
+            updateD3(selNode, false);
+          }
+          return;
+        }
+        moveY = -PAN_INC * slow; // pan up
+        break;
+      case KEY_RIGHT:
+        if (shift) { // move selection to first/last child
+          if (!selNode) {
+            selectNode(root);
+          } else {
+            if (selNode.children && selNode.children.length > 0) {
+              selectNode(selNode.children[0]);
+              updateD3(selNode, false);
+            }
+          }
+          return;
+        }
+        moveX = PAN_INC * slow; // pan right
+        break;
+      case KEY_DOWN:
+        if (shift) { // move selection to next child
+          if (!selNode) {
+            selectNode(root);
+          } else if (selNode.parent) {
+            parch = selNode.parent.children;
+            selectNode(parch[(parch.indexOf(selNode) + 1) % parch.length]);
+            updateD3(selNode, false);
+          }
+          return;
+        }
+        moveY = PAN_INC * slow; // pan down
+        break;
+      case KEY_RETURN:
+        if (!selNode) {
+          selectNode(root);
+        }
+        if (shift) { // show loaded
+          expandTree(selNode);
+          loadSubItems(selNode);
+        } else {
+          toggle(selNode); // expand/collapse node
+        }
+        updateD3(selNode, true);
+        return;
+      case KEY_SPACE:
+        if (!selNode) {
+          selectNode(root);
+        }
+        if (shift) { // browse into
+          browseInto(selNode);
+        } else { // load +1 level
+          expand1Level(selNode);
+          updateD3(selNode, true);
+        }
+        return;
+      case KEY_HOME: // reset transform
+        curX = width / 2;
+        curY = height / 2;
+        curR = limitR(90 - root.x);
+        curZ = 1;
+        updateD3(root, true);
+        return;
+      case KEY_END: // zoom to selection
+        if (!selNode) {
+          return;
+        }
+        curX = width / 2 - selNode.y * curZ;
+        curY = height / 2;
+        curR = limitR(90 - selNode.x);
+        updateD3(selNode, true);
+        return;
+      default:
+        return; // ignore other keys
+    }
+    keysdown.push(key);
+    // start animation if anything happening
+    if (keysdown.length > 0 && animation === null) {
+      animation = requestAnimationFrame(frame);
+    }
+  }
+
+  function actionUp(key) {
+    var pos = keysdown.indexOf(key);
+    if (pos < 0) {
+      return;
+    }
+
+    switch (key) {
+      case KEY_PLUS: // - = zoom out
+      case KEY_MINUS: // + = zoom in
+        moveZ = 0;
+        break;
+      case KEY_PAGEUP: // page up = rotate CCW
+      case KEY_PAGEDOWN: // page down = rotate CW
+        moveR = 0;
+        break;
+      case KEY_LEFT: // left arrow
+      case KEY_RIGHT: // right arrow
+        moveX = 0;
+        break;
+      case KEY_UP: // up arrow
+      case KEY_DOWN: // down arrow
+        moveY = 0;
+        break;
+    }
+    keysdown.splice(pos, 1); // remove key
+    if (keysdown.length > 0 || animation === null) {
+      return;
+    }
+    cancelAnimationFrame(animation);
+    animation = aniTime = null;
+    networkElem.focus();
+  }
+
+  return new D3Widget(browseState, browseEvents);
+}
\ No newline at end of file
diff --git a/src/lib/mercury/dialog-click-hook.js b/src/lib/mercury/dialog-click-hook.js
new file mode 100644
index 0000000..b8db190
--- /dev/null
+++ b/src/lib/mercury/dialog-click-hook.js
@@ -0,0 +1,19 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+ * Because of the way Mercury captures and delegates events, ev-click does not
+ * work for items inside a dialog because polymer moves the dialog's DOM around
+ * This hook is a work-around for that issue.
+ */
+module.exports = function(handler) {
+  return Object.create({
+    hook: function(elem) {
+      if (!elem.clickHandlerInstalled) {
+        elem.addEventListener('click', handler);
+        elem.clickHandlerInstalled = true;
+      }
+    }
+  });
+};
\ No newline at end of file
diff --git a/src/services/namespace/service.js b/src/services/namespace/service.js
index 4d442f1..2e58542 100644
--- a/src/services/namespace/service.js
+++ b/src/services/namespace/service.js
@@ -27,7 +27,9 @@
   search: search,
   util: naming,
   initVanadium: getRuntime,
-  clearCache: clearCache
+  clearCache: clearCache,
+  deleteMountPoint: deleteMountPoint,
+  prefixes: prefixes
 };
 
 //TODO(aghassemi) What's a good timeout? It should be shorter than this.
@@ -225,6 +227,19 @@
 }
 
 /*
+ * Deletes a mount point.
+ * @param {string} name mountpoint name to delete.
+ * @return {Promise<void>} Success or failure promise.
+ */
+function deleteMountPoint(name) {
+  return getRuntime().then(function(rt) {
+    var ctx = rt.getContext().withTimeout(RPC_TIMEOUT);
+    var ns = rt.namespace();
+    return ns.delete(ctx, name, true);
+  });
+}
+
+/*
  * Given a name, provide information about its mounttable objectAddress.
  * @param {string} objectName Object name to get mounttable objectAddress for.
  * @return {Promise.<mercury.array<string>>} Promise of an array of
@@ -424,13 +439,17 @@
   function clearByPrefix(cache, parent) {
     var keys = cache.keys();
     keys.forEach(function(key) {
-      var isMatch =
-        (key === parent) ||
-        (key.lastIndexOf(naming.clean(parent) + '/') === 0);
-
-      if (isMatch) {
+      if (prefixes(parent, key)) {
         cache.del(key);
       }
     });
   }
+}
+
+/*
+ * Returns true iff parentName is a parent of childName or is same as childName
+ */
+function prefixes(parentName, childName) {
+  return (parentName === childName) ||
+    (childName.indexOf(naming.clean(parentName) + '/') === 0);
 }
\ No newline at end of file
diff --git a/web-component-dependencies.html b/web-component-dependencies.html
index 2fd7819..2597464 100644
--- a/web-component-dependencies.html
+++ b/web-component-dependencies.html
@@ -9,6 +9,8 @@
 and the browserify transforms handles the bundling of wc dependencies at runtime.
 -->
 <link rel="import" href="bower_components/core-drawer-panel/core-drawer-panel.html">
+<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
+<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
 <link rel="import" href="bower_components/core-header-panel/core-header-panel.html">
 <link rel="import" href="bower_components/core-icons/av-icons.html">
 <link rel="import" href="bower_components/core-icons/core-icons.html">