Ver código fonte

[zookeeper] Ported app to Hue3 style

Enrico Berti 12 anos atrás
pai
commit
25981546e4

BIN
apps/zookeeper/src/zookeeper/static/art/icon_24.png


+ 10 - 35
apps/zookeeper/src/zookeeper/static/css/zookeeper.css

@@ -16,44 +16,19 @@
 // limitations under the License.
 */
 
-.zookeeper img.zookeeper_icon {
-	width: 55px;
-	height: 55px;
-	position: absolute;
-	top: 27px;
-	left: 3px;
+.simpler {
+  padding-left: 4px!important;
 }
 
-div.zookeeper .left_col li {
-    margin: 5px 0px;
-    font-size: 16px;
-    background-color: white;
-    color: black;
-    padding: 2px 1px 1px 5px;
-    -moz-border-radius: 3px;
-    -webkit-border-radius: 3px;
-    border: solid black 1px;
+.delete {
+  padding: 2px;
+  cursor: pointer;
 }
 
-div.zookeeper .left_col li:hover {
-    background-color: lightBlue;
-    color: white;
+textarea {
+  width: 95%;
 }
 
-div.zookeeper .left_col li a {
-    color: black;
-    display: block;
-}
-
-div.zookeeper .left_col li a:hover {
-    text-decoration: none;
-}
-
-div.zookeeper td a.delete {
-//  background: url(/zookeeper/static/art/line_icons.png) no-repeat 0 -70px;
-}
-
-div.zookeeper .createZnodeForm td,
-div.zookeeper .editZnodeForm td {
-  padding: 5px;
-}
+.tab-pane {
+  margin-bottom: 30px;
+}

+ 216 - 0
apps/zookeeper/src/zookeeper/static/js/base64.js

@@ -0,0 +1,216 @@
+/*
+Copyright (c) 2008 Fred Palmer fred.palmer_at_gmail.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+*/
+function StringBuffer()
+{ 
+    this.buffer = []; 
+} 
+
+StringBuffer.prototype.append = function append(string)
+{ 
+    this.buffer.push(string); 
+    return this; 
+}; 
+
+StringBuffer.prototype.toString = function toString()
+{ 
+    return this.buffer.join(""); 
+}; 
+
+var Base64 =
+{
+    codex : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
+
+    encode : function (input)
+    {
+        var output = new StringBuffer();
+
+        var enumerator = new Utf8EncodeEnumerator(input);
+        while (enumerator.moveNext())
+        {
+            var chr1 = enumerator.current;
+
+            enumerator.moveNext();
+            var chr2 = enumerator.current;
+
+            enumerator.moveNext();
+            var chr3 = enumerator.current;
+
+            var enc1 = chr1 >> 2;
+            var enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+            var enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+            var enc4 = chr3 & 63;
+
+            if (isNaN(chr2))
+            {
+                enc3 = enc4 = 64;
+            }
+            else if (isNaN(chr3))
+            {
+                enc4 = 64;
+            }
+
+            output.append(this.codex.charAt(enc1) + this.codex.charAt(enc2) + this.codex.charAt(enc3) + this.codex.charAt(enc4));
+        }
+
+        return output.toString();
+    },
+
+    decode : function (input)
+    {
+        var output = new StringBuffer();
+
+        var enumerator = new Base64DecodeEnumerator(input);
+        while (enumerator.moveNext())
+        {
+            var charCode = enumerator.current;
+
+            if (charCode < 128)
+                output.append(String.fromCharCode(charCode));
+            else if ((charCode > 191) && (charCode < 224))
+            {
+                enumerator.moveNext();
+                var charCode2 = enumerator.current;
+
+                output.append(String.fromCharCode(((charCode & 31) << 6) | (charCode2 & 63)));
+            }
+            else
+            {
+                enumerator.moveNext();
+                var charCode2 = enumerator.current;
+
+                enumerator.moveNext();
+                var charCode3 = enumerator.current;
+
+                output.append(String.fromCharCode(((charCode & 15) << 12) | ((charCode2 & 63) << 6) | (charCode3 & 63)));
+            }
+        }
+
+        return output.toString();
+    }
+}
+
+
+function Utf8EncodeEnumerator(input)
+{
+    this._input = input;
+    this._index = -1;
+    this._buffer = [];
+}
+
+Utf8EncodeEnumerator.prototype =
+{
+    current: Number.NaN,
+
+    moveNext: function()
+    {
+        if (this._buffer.length > 0)
+        {
+            this.current = this._buffer.shift();
+            return true;
+        }
+        else if (this._index >= (this._input.length - 1))
+        {
+            this.current = Number.NaN;
+            return false;
+        }
+        else
+        {
+            var charCode = this._input.charCodeAt(++this._index);
+
+            // "\r\n" -> "\n"
+            //
+            if ((charCode == 13) && (this._input.charCodeAt(this._index + 1) == 10))
+            {
+                charCode = 10;
+                this._index += 2;
+            }
+
+            if (charCode < 128)
+            {
+                this.current = charCode;
+            }
+            else if ((charCode > 127) && (charCode < 2048))
+            {
+                this.current = (charCode >> 6) | 192;
+                this._buffer.push((charCode & 63) | 128);
+            }
+            else
+            {
+                this.current = (charCode >> 12) | 224;
+                this._buffer.push(((charCode >> 6) & 63) | 128);
+                this._buffer.push((charCode & 63) | 128);
+            }
+
+            return true;
+        }
+    }
+}
+
+function Base64DecodeEnumerator(input)
+{
+    this._input = input;
+    this._index = -1;
+    this._buffer = [];
+}
+
+Base64DecodeEnumerator.prototype =
+{
+    current: 64,
+
+    moveNext: function()
+    {
+        if (this._buffer.length > 0)
+        {
+            this.current = this._buffer.shift();
+            return true;
+        }
+        else if (this._index >= (this._input.length - 1))
+        {
+            this.current = 64;
+            return false;
+        }
+        else
+        {
+            var enc1 = Base64.codex.indexOf(this._input.charAt(++this._index));
+            var enc2 = Base64.codex.indexOf(this._input.charAt(++this._index));
+            var enc3 = Base64.codex.indexOf(this._input.charAt(++this._index));
+            var enc4 = Base64.codex.indexOf(this._input.charAt(++this._index));
+
+            var chr1 = (enc1 << 2) | (enc2 >> 4);
+            var chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+            var chr3 = ((enc3 & 3) << 6) | enc4;
+
+            this.current = chr1;
+
+            if (enc3 != 64)
+                this._buffer.push(chr2);
+
+            if (enc4 != 64)
+                this._buffer.push(chr3);
+
+            return true;
+        }
+    }
+};

+ 11 - 6
apps/zookeeper/src/zookeeper/templates/clients.mako

@@ -21,14 +21,19 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader(_('Clients'), app_name, user, '100px') | n,unicode }
-${ shared.header("ZooKeeper Browser > Clients > %s:%s" % (host, port)) }
+${ commonheader(_('Clients'), app_name, user, '60px') | n,unicode }
+<%
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')],
+    [cluster['nice_name'].lower(), url('zookeeper:view', id=cluster['id'])],
+    [host + ":" + port, url('zookeeper:clients', id=cluster['id'], host=host + ":" + port)]
+  ]
+%>
 
-<h1>${host}:${port} :: client connections</h1>
-<br />
+${ shared.header(_breadcrumbs, clusters) }
 
 % if clients:
-  <table data-filters="HtmlTable">
+  <table class="table">
   <thead>
     <tr>
       <th>Host</th>
@@ -51,6 +56,6 @@ ${ shared.header("ZooKeeper Browser > Clients > %s:%s" % (host, port)) }
   </table>
 % endif
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+${ shared.footer() }
 
 ${ commonfooter(messages) | n,unicode }

+ 20 - 11
apps/zookeeper/src/zookeeper/templates/create.mako

@@ -21,22 +21,31 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader("ZooKeeper Browser > Create Znode", app_name, user, '100px') | n,unicode }
-${ shared.header(clusters) }
+${ commonheader("ZooKeeper Browser > Create Znode", app_name, user, '60px') | n,unicode }
+<%
+  _split = path.split("/");
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')],
+    [cluster['nice_name'].lower(), url('zookeeper:view', id=cluster['id'])],
+  ]
+  for idx, p in enumerate(_split):
+    if p != "":
+      _breadcrumbs.append([p, url('zookeeper:tree', id=cluster['id'], path= "/".join(_split[:idx+1]))]);
+
+  _breadcrumbs.append([_('New Znode'), ""]);
+%>
 
-<h2>Create New Znode :: ${path}</h2>
-<hr /><br />
+${ shared.header(_breadcrumbs, clusters) }
 
 <form class="createZnodeForm" action="" method="POST">
-  <table align="center">
-    ${form.as_table()|n}
-    <tr><td colspan="2" align="right">
-    <button type="submit">Create</button>
-    </td></tr>
-  </table>
+  ${form.as_table()|n}
+  <br/>
+  <br/>
+  <input type="submit" class="btn btn-primary" value="${ _('Create Znode') }">
+  <a class="btn" href="${url('zookeeper:tree', id=cluster['id'], path=path)}">${ _('Cancel') }</a>
 </form>
 
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+${ shared.footer() }
 
 ${ commonfooter(messages) | n,unicode }

+ 23 - 14
apps/zookeeper/src/zookeeper/templates/edit.mako

@@ -21,22 +21,31 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader("ZooKeeper Browser > Edit Znode > %s" % path, app_name, user, '100px') | n,unicode }
-${ shared.header(clusters) }
-
-<h2>Edit Znode Data :: ${path}</h2>
-<hr /><br />
-
-<form class="editZnodeForm" action="" method="POST">
-  <table align="center">
-    ${form.as_table()|n}
-    <tr><td colspan="2" align="right">
-    <button type="submit">Save</button>
-    </td></tr>
-  </table>
+${ commonheader("ZooKeeper Browser > Edit Znode > %s" % path, app_name, user, '60px') | n,unicode }
+
+<%
+  _split = path.split("/");
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')],
+    [cluster['nice_name'].lower(), url('zookeeper:view', id=cluster['id'])],
+  ]
+  for idx, p in enumerate(_split):
+    if p != "":
+      _breadcrumbs.append([p, url('zookeeper:tree', id=cluster['id'], path= "/".join(_split[:idx+1]))]);
+
+  _breadcrumbs.append([_('Edit Znode'), ""]);
+%>
+
+${ shared.header(_breadcrumbs,clusters) }
+
+<form action="" method="POST">
+  ${form.as_table()|n}
+  <br/>
+  <input type="submit" class="btn btn-primary" value="${ _('Save') }">
+  <a class="btn" href="${url('zookeeper:tree', id=cluster['id'], path=path)}">${ _('Cancel') }</a>
 </form>
 
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+${ shared.footer() }
 
 ${ commonfooter(messages) | n,unicode }

+ 36 - 28
apps/zookeeper/src/zookeeper/templates/index.mako

@@ -21,42 +21,50 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader(_('View'), app_name, user, '100px') | n,unicode }
-${ shared.header(clusters) }
+${ commonheader(_('View'), app_name, user, '60px') | n,unicode }
 
+<%
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')]
+  ]
+%>
 
-<h2>Overview</h2>
 
-<br />
+${ shared.header(_breadcrumbs, clusters) }
 
 % for i, c in enumerate(clusters):
-  <h3> ${ i + 1 }. <a href="${ url('zookeeper:view', id=i) }">${ c } Cluster Overview</a></h3><br />
-
-  <table data-filters="HtmlTable">
-  <thead>
-    <tr>
-      <th>Node</th>
-      <th>Role</th>
-      <th>Avg Latency</th>
-      <th>Watch Count</th>
-      <th>Version</th>
-    </tr>
-  </thead>
-  % for host, stats in overview[c].items():
-    <tr>
-      <td><a href="${ url('zookeeper:view', id=i) }" data-row-selector="true">${ host }</a></td>
-      <td>${stats.get('zk_server_state', '')}</td>
-      <td>${stats.get('zk_avg_latency', '')}</td>
-      <td>${stats.get('zk_watch_count', '')}</td>
-      <td>${stats.get('zk_version', '')}</td>
-    </tr>
-  % endfor
+  <h3 class="card-heading simple simpler">${ _('Cluster') } <a href="${ url('zookeeper:view', id=i) }">${ c }</a></h3>
+  <table class="table">
+    <thead>
+      <tr>
+        <th>Node</th>
+        <th>Role</th>
+        <th>Avg Latency</th>
+        <th>Watch Count</th>
+        <th>Version</th>
+      </tr>
+    </thead>
+    <tbody>
+    % for host, stats in overview[c].items():
+      <tr>
+        <td><a href="${ url('zookeeper:clients', id=i, host=host) }" data-row-selector="true">${ host }</a></td>
+        <td>${stats.get('zk_server_state', '')}</td>
+        <td>${stats.get('zk_avg_latency', '')}</td>
+        <td>${stats.get('zk_watch_count', '')}</td>
+        <td>${stats.get('zk_version', '')}</td>
+      </tr>
+    % endfor
+    </tbody>
   </table>
-
-  <br /><br />
 % endfor
 
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+${ shared.footer() }
+
+<script type="text/javascript" charset="utf-8">
+  $(document).ready(function () {
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
+</script>
 
 ${ commonfooter(messages) | n,unicode }

+ 47 - 19
apps/zookeeper/src/zookeeper/templates/shared_components.mako

@@ -15,29 +15,57 @@
 ## limitations under the License.
 
 <%!
-from django.template.defaultfilters import urlencode, escape
+  from django.template.defaultfilters import urlencode, escape
+  from django.utils.translation import ugettext as _
 %>
 
-<%def name="header(clusters)">
-    <div data-filters="SplitView">
-    <div class="left_col jframe_padded" style="width:150px;">
-        <h2>Clusters</h2>
-        <ul>
+<%def name="header(breadcrumbs, clusters, withBody=True)">
+  <div class="container-fluid">
+  <div class="row-fluid">
+    <div class="card">
+      <h1 class="card-heading simple">
+        <div class="btn-group pull-right">
+          <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+            ${ _('Go to cluster') }
+            <span class="caret"></span>
+          </a>
+          <ul class="dropdown-menu">
             % for id, c in enumerate(clusters):
-                <li>
-                  <a href="${ url('zookeeper:view', id=id) }">
-                    ${ c }
-                  </a>
-                </li>
+              <li>
+                <a href="${ url('zookeeper:view', id=id) }">
+                  ${ c }
+                </a>
+              </li>
             % endfor
-        </ul>
-    </div>
-</%def>
+          </ul>
+        </div>
+      % for idx, crumb in enumerate(breadcrumbs):
+        %if crumb[1] != "":
+          <a href="${crumb[1]}">${crumb[0]}</a>
+        %else:
+          ${crumb[0]}
+        %endif
 
+        %if idx < len(breadcrumbs) - 1:
+          &gt;
+        %endif
+      % endfor
+      </h1>
+      %if withBody:
+      <div class="card-body">
+        <p>
+      %endif
+</%def>
 
-<%def name="info_button(url, text)">
-  <a data-filters="ArtButton" href="${url}" style="background: url(/static/art/info.png) left 50%; padding: 6px 6px 6px 20px; margin: 10px;"
-    data-icon-styles="{'width': 14, 'height': 14}">
-      ${text}
-  </a>
+<%def name="footer(withBody=True)">
+      %if withBody:
+        </p>
+      </div>
+      %endif
+    </div>
+  </div>
+</div>
+<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+<script src="/zookeeper/static/js/base64.js" type="text/javascript" charset="utf-8"></script>
 </%def>
+

+ 112 - 50
apps/zookeeper/src/zookeeper/templates/tree.mako

@@ -21,63 +21,125 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader("ZooKeeper Browser > Tree > %s > %s" % (cluster['nice_name'], path), app_name, user, '100px') | n,unicode }
-${ shared.header(clusters) }
+${ commonheader("ZooKeeper Browser > Tree > %s > %s" % (cluster['nice_name'], path), app_name, user, '60px') | n,unicode }
 
+<%
+  _split = path.split("/");
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')],
+    [cluster['nice_name'].lower(), url('zookeeper:view', id=cluster['id'])],
+  ]
+  for idx, p in enumerate(_split):
+    if p != "":
+      _breadcrumbs.append([p, url('zookeeper:tree', id=cluster['id'], path= "/".join(_split[:idx+1]))]);
+%>
 
-<h1>${cluster['nice_name'].lower()} :: ${path}</h1>
-<br />
-
-<table data-filters="HtmlTable">
-  <thead>
-  <th colspan="2">Children</th>
-  </thead>
-  % for child in children:
-    <tr><td width="100%">
-      <a href="${url('zookeeper:tree', id=cluster['id'], \
-          path=("%s/%s" % (path, child)).replace('//', '/'))}">
-      ${child}</a>
-    </td><td>
-      <a title="Delete ${child}" class="delete frame_tip confirm_and_post" alt="Are you sure you want to delete ${child}?" href="${url('zookeeper:delete', id=cluster['id'], \
-          path=("%s/%s" % (path, child)).replace('//', '/'))}">Delete</a>
-    </td></tr>
-  % endfor
-</table>
-<br />
-<span style="float: right">
-  ${shared.info_button(url('zookeeper:create', id=cluster['id'], path=path), 'Create New')}
-</span>
-
-<div style="clear: both"></div>
-
-<h2>data :: base64 :: length :: ${znode.get('dataLength', 0)}</h2>
-<br />
 
-<textarea name="data64" style="width: 100%;" rows="5" readonly="readonly">${znode.get('data64', '')}</textarea>
-<div style="clear: both"></div>
-<span style="float: right">
-  ${shared.info_button(url('zookeeper:edit_as_base64', id=cluster['id'], path=path), 'Edit as Base64')}
-  ${shared.info_button(url('zookeeper:edit_as_text', id=cluster['id'], path=path), 'Edit as Text')}
-</span>
-<div style="clear: both"></div>
-<br />
+${ shared.header(_breadcrumbs, clusters, False) }
+<div class="row-fluid" style="margin-top: 20px">
+  <div class="span3">
+    <div class="sidebar-nav">
+      <ul class="nav nav-list" style="border: 0">
+        <li class="nav-header">${ _('Znodes') }</li>
+        % for child in children:
+        <li>
+          <a href="${url('zookeeper:tree', id=cluster['id'], path=("%s/%s" % (path, child)).replace('//', '/'))}">${child}</a>
+        </li>
+        % endfor
+        % if len(children) == 0:
+          <li class="white">${ _('No children available') }</li>
+        % endif
+        <li class="white">
+          <button class="btn" onclick="location.href='${url('zookeeper:create', id=cluster['id'], path=path)}'"><i class="icon-plus-sign"></i> ${ _('Add') }</button>
+          <button id="removeBtn" class="btn btn-danger disable-feedback" data-msg="${_('Are you sure you want to delete %s?' % path)}" data-url="${url('zookeeper:delete', id=cluster['id'], path=path)}"><i class="icon-remove-sign"></i> ${ _('Remove current ZNode') }</button>
+        </li>
+      </ul>
+    </div>
+  </div>
+  <div class="span9">
+    <ul class="nav nav-tabs">
+      %if znode.get('dataLength', 0) != 0:
+        <li class="active"><a href="#base64" data-toggle="tab">Base64 (${znode.get('dataLength', 0)})</a></li>
+        <li><a href="#text" data-toggle="tab">${ _('Text') }</a></li>
+        <li><a href="#stats" data-toggle="tab">${ _('Stats') }</a></li>
+      %else:
+        <li><a href="#stats" data-toggle="tab">${ _('Stats') }</a></li>
+      %endif
+    </ul>
+    <div class="tab-content">
+      %if znode.get('dataLength', 0) != 0:
+      <div class="tab-pane active" id="base64">
+        <textarea id="textarea64" rows="14" readonly="readonly">${znode.get('data64', '')}</textarea>
+        <a href="${url('zookeeper:edit_as_base64', id=cluster['id'], path=path)}" class="btn"><i class="icon-pencil"></i> ${_('Edit as Base64')}</a>
+      </div>
+      <div class="tab-pane" id="text">
+        <textarea id="textareaText" rows="14" readonly="readonly"></textarea>
+        <a href="${url('zookeeper:edit_as_text', id=cluster['id'], path=path)}" class="btn"><i class="icon-pencil"></i> ${_('Edit as Text')}</a>
+      </div>
+      <div class="tab-pane" id="stats">
+      %else:
+      <div class="tab-pane active" id="stats">
+      %endif
+        <table class="table">
+          <thead>
+          <tr>
+            <th width="20%">Key</th>
+            <th>Value</th>
+          </tr>
+          </thead>
+          % for key in ('pzxid', 'ctime', 'aversion', 'mzxid', 'ephemeralOwner', 'version', 'mtime', 'cversion', 'czxid'):
+          <tr>
+            <td>${key}</td>
+            <td>${znode[key]}</td>
+          </tr>
+          % endfor
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
 
-<h2>stat information</h2>
-<br />
 
-<table data-filters="HtmlTable">
-  <thead><tr><th>Key</th>
-    <th width="80%">Value</th></tr></thead>
-  % for key in ('pzxid', 'ctime', 'aversion', 'mzxid', \
-      'ephemeralOwner', 'version', 'mtime', 'cversion', 'czxid'):
-    <tr><td>${key}</td><td>${znode[key]}</td></tr>
-  % endfor
-</table>
+${ shared.footer() }
 
-<br />
-<a target="_blank" href="http://hadoop.apache.org/zookeeper/docs/current/zookeeperProgrammers.html#sc_zkStatStructure">Details on stat information.</a>
+<div id="removeModal" class="modal hide fade">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${ _('Delete Znode?') }</h3>
+  </div>
+  <div class="modal-body">
+    <p class="question"></p>
+  </div>
+  <div class="modal-footer">
+    <a href="#" class="btn" data-dismiss="modal">${ _('Cancel') }</a>
+    <a id="removeBtnModal" href="#" class="btn btn-danger disable-feedback">${ _('Yes, delete it!') }</a>
+  </div>
+</div>
 
+<script type="text/javascript" charset="utf-8">
+  $(document).ready(function () {
+  %if znode.get('dataLength', 0) != 0:
+    var txt = Base64.decode($("#textarea64").val());
+    $("#textareaText").val(txt);
+  %endif
+    $("#removeModal").modal({
+      show: false
+    });
+    $("#removeBtn").on("click", function(){
+      var _url = $(this).data("url");
+      var _msg = $(this).data("msg");
+      $("#removeModal").find(".question").text(_msg);
+      $("#removeBtnModal").data("url", _url);
+      $("#removeModal").modal("show");
+    });
+    $("#removeBtnModal").on("click", function(){
+      $(this).addClass("disabled");
+      $.post($(this).data("url"), function(data){
+        location.href = data.redirect;
+      })
+    })
+  });
+</script>
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
 
 ${ commonfooter(messages) | n,unicode }

+ 129 - 105
apps/zookeeper/src/zookeeper/templates/view.mako

@@ -21,116 +21,140 @@
 
 <%namespace name="shared" file="shared_components.mako" />
 
-${ commonheader(_('View'), app_name, user, '100px') | n,unicode }
-${ shared.header(clusters) }
-
+${ commonheader(_('View'), app_name, user, '60px') | n,unicode }
 
+<%
+  _breadcrumbs = [
+    ["ZooKeeper Browser", url('zookeeper:index')],
+    [cluster['nice_name'].lower(), url('zookeeper:view', id=cluster['id'])]
+  ]
+%>
 <%def name="show_stats(stats)">
-    <thead>
-      <tr><th>Key</th>
-      <th width="100%">Value</th></tr>
-    </thead>
-
-    <tr><td>Version</td>
-      <td>${stats.get('zk_version')}</td>
-    </tr>
-
-    <tr><td>Latency</td><td>
-      Min: ${stats.get('zk_min_latency', '')}
-      Avg: ${stats.get('zk_avg_latency', '')}
-      Max: ${stats.get('zk_max_latency', '')}
-    </td></tr>
-
-    <tr><td>Packets</td>
-      <td>Sent: ${stats.get('zk_packets_sent', '')}
-      Received: ${stats.get('zk_packets_received', '')}
-      </td>
-    </tr>
-
-    <tr><td>Outstanding Requests</td>
-      <td>${stats.get('zk_outstanding_requests', '')}</td>
-    </tr>
-
-    <tr><td>Watch Count</td>
-      <td>${stats.get('zk_watch_count', '')}</td>
-    </tr>
-
-    <tr><td>Open FD Count</td>
-      <td>${stats.get('zk_open_file_descriptor_count', '')}</td>
-    </tr>
-
-    <tr><td>Max FD Count</td>
-      <td>${stats.get('zk_max_file_descriptor_count', '')}</td>
-    </tr>
-
-</%def>
-
-<h2> ${ cluster['nice_name'] } Cluster Overview </h2>
-
-${ shared.info_button(url('zookeeper:tree', id=cluster['id'], path='/'), 'View Znode Hierarchy') }
-
-<br /><br />
-
-% if leader:
-<h2>General</h2>
-
-<table data-filters="HtmlTable">
   <thead>
-    <tr><th>Key</th><th width="100%">Value</th></tr>
+  <tr>
+    <th width="20%">${ _('Key') }</th>
+    <th>${ _('Value') }</th>
+  </tr>
   </thead>
-
-  <tr><td>ZNode Count</td>
-    <td>${leader.get('zk_znode_count', '')}</td></tr>
-
-  <tr><td>Ephemerals Count</td>
-    <td>${leader.get('zk_ephemerals_count', '')}</td></tr>
-
-  <tr><td>Approximate Data Size</td>
-    <td>${leader.get('zk_approximate_data_size', '')} bytes</td></tr>
-
-</table>
-<br /><br />
-% endif
-
-% if leader:
-  <h2>node :: ${leader['host']} :: leader</h2>
-
-  ${shared.info_button(url('zookeeper:clients', host=leader['host']), 'View Client Connections')}
-
-  <br /><br />
-  <table data-filters="HtmlTable">
-    ${show_stats(leader)}
-
-    <tr><td>Followers</td>
-      <td>${leader.get('zk_followers', '')}</td>
-    </tr>
-
-    <tr><td>Synced Followers</td>
-      <td>${leader.get('zk_synced_followers', '')}</td>
-    </tr>
-
-    <tr><td>Pending Syncs</td>
-      <td>${leader.get('zk_pending_syncs', '')}</td>
-    </tr>
-
-  </table>
-<br /><br />
-% endif
-
-% for stats in followers:
-  <h2>node :: ${stats['host']} :: follower</h2>
-  <br />
-
-  ${shared.info_button(url('zookeeper:clients', host=stats['host']), 'View Client Connections')}
-
-  <br /><br />
-  <table data-filters="HtmlTable">
-    ${show_stats(stats)}
-  </table>
-  <br /><br />
-% endfor
+  <tr>
+    <td>${ _('Version') }</td>
+    <td>${stats.get('zk_version')}</td>
+  </tr>
+
+  <tr>
+    <td>${ _('Latency') }</td>
+    <td>
+      ${ _('Min:') } ${stats.get('zk_min_latency', '')}
+      ${ _('Avg:') } ${stats.get('zk_avg_latency', '')}
+      ${ _('Max:') } ${stats.get('zk_max_latency', '')}
+    </td>
+  </tr>
+
+  <tr>
+    <td>${ _('Packets') }</td>
+    <td>${ _('Sent:') } ${stats.get('zk_packets_sent', '')}
+      ${ _('Received:') } ${stats.get('zk_packets_received', '')}
+    </td>
+  </tr>
+
+  <tr>
+    <td>${ _('Outstanding Requests') }</td>
+    <td>${stats.get('zk_outstanding_requests', '')}</td>
+  </tr>
+
+  <tr>
+    <td>${ _('Watch Count') }</td>
+    <td>${stats.get('zk_watch_count', '')}</td>
+  </tr>
+
+  <tr>
+    <td>${ _('Open FD Count') }</td>
+    <td>${stats.get('zk_open_file_descriptor_count', '')}</td>
+  </tr>
+
+  <tr>
+    <td>${ _('Max FD Count') }</td>
+    <td>${stats.get('zk_max_file_descriptor_count', '')}</td>
+  </tr>
+</%def>
 
 
-<link rel="stylesheet" href="/zookeeper/static/css/zookeeper.css">
+${ shared.header(_breadcrumbs, clusters, False) }
+<div class="row-fluid" style="margin-top: 20px">
+  <div class="span3">
+    <div class="sidebar-nav">
+      <ul class="nav nav-list" style="border: 0">
+        <li class="nav-header">${ _('Znodes') }</li>
+        <li><a href="${url('zookeeper:tree', id=cluster['id'], path='/')}"> ${ _('View Znode Hierarchy') }</a></li>
+      </ul>
+    </div>
+  </div>
+  <div class="span9">
+    % if leader:
+    <h2 class="card-heading simple simpler">${ _('General') }</h2>
+    <table class="table">
+      <thead>
+      <tr>
+        <th width="20%">${ _('Key') }</th>
+        <th>${ _('Value') }</th>
+      </tr>
+      </thead>
+      <tr>
+        <td>${ _('ZNode Count') }</td>
+        <td>${leader.get('zk_znode_count', '')}</td>
+      </tr>
+
+      <tr>
+        <td>${ _('Ephemerals Count') }</td>
+        <td>${leader.get('zk_ephemerals_count', '')}</td>
+      </tr>
+
+      <tr>
+        <td>${ _('Approximate Data Size') }</td>
+        <td>${leader.get('zk_approximate_data_size', '')} bytes</td>
+      </tr>
+
+    </table>
+    % endif
+
+    % if leader:
+      <h2 class="card-heading simple simpler">
+        <div class="pull-right"><a href="${url('zookeeper:clients', id=cluster['id'], host=leader['host'])}"><i class="icon-eye-open"></i> ${_('Client Connections')}</a></div>
+        ${ _('Node') } ${leader['host']} (${ _('leader') })
+      </h2>
+
+      <table class="table">
+        ${show_stats(leader)}
+
+        <tr><td>${ _('Followers') }</td>
+          <td>${leader.get('zk_followers', '')}</td>
+        </tr>
+
+        <tr><td>${ _('Synced Followers') }</td>
+          <td>${leader.get('zk_synced_followers', '')}</td>
+        </tr>
+
+        <tr><td>${ _('Pending Syncs') }</td>
+          <td>${leader.get('zk_pending_syncs', '')}</td>
+        </tr>
+
+      </table>
+    % endif
+
+    % for stats in followers:
+      <h2 class="card-heading simple simpler">
+        <div class="pull-right"><a href="${url('zookeeper:clients', id=cluster['id'], host=stats['host'])}"><i class="icon-eye-open"></i> ${ _('Client Connections') }</a></div>
+      ${ _('Node') } ${stats['host']} (${ _('follower') })
+      </h2>
+      <table class="table">
+        ${show_stats(stats)}
+      </table>
+    % endfor
+
+  </div>
+</div>
+
+
+${ shared.footer() }
 
 ${ commonfooter(messages) | n,unicode }

+ 2 - 2
apps/zookeeper/src/zookeeper/urls.py

@@ -21,10 +21,10 @@ from django.conf.urls.defaults import patterns, url
 urlpatterns = patterns('zookeeper.views',
   url(r'^$', 'index', name='index'),
   url(r'view/(?P<id>\d+)$', 'view', name='view'),
-  url(r'clients/(?P<host>.+)$', 'clients', name='clients'),
+  url(r'clients/(?P<id>\d+)/(?P<host>.+)$', 'clients', name='clients'),
   url(r'tree/(?P<id>\d+)(?P<path>.+)$', 'tree', name='tree'),
   url(r'create/(?P<id>\d+)(?P<path>.*)$', 'create', name='create'),
   url(r'delete/(?P<id>\d+)(?P<path>.*)$', 'delete', name='delete'),
   url(r'edit/base64/(?P<id>\d+)(?P<path>.*)$', 'edit_as_base64', name='edit_as_base64'),
-  url(r'edit/text/(?P<id>\d+)(?P<path>.*)$', 'edit_as_text', name='edit_as_text')
+  url(r'edit/text/(?P<id>\d+)(?P<path>.*)$', 'edit_as_text', name='edit_as_text'),
 )

+ 29 - 11
apps/zookeeper/src/zookeeper/views.py

@@ -15,8 +15,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+try:
+  import json
+except ImportError:
+  import simplejson as json
+
+import logging
+
 from desktop.lib.django_util import render
-from django.http import Http404
+from django.http import Http404, HttpResponse
+from django.core.urlresolvers import reverse
 
 from zookeeper import settings
 
@@ -49,7 +57,7 @@ def _group_stats_by_role(stats):
   for host, stats in stats.items():
     stats['host'] = host
 
-    if stats.get('zk_server_state') == 'leader':
+    if stats.get('zk_server_state') == 'leader' or stats.get('zk_server_state') == 'standalone':
       leader = stats
 
     elif stats.get('zk_server_state') == 'follower':
@@ -74,12 +82,14 @@ def view(request, id):
   leader, followers = _group_stats_by_role(stats)
 
   return render('view.mako', request, {
-      'cluster': cluster, 'leader': leader, 'followers': followers,
+      'cluster': cluster, 'all_stats': stats, 'leader': leader, 'followers': followers,
       'clusters': CLUSTERS.get(),
   })
 
 
-def clients(request, host):
+def clients(request, id, host):
+  cluster = get_cluster_or_404(id)
+
   parts = host.split(':')
   if len(parts) != 2:
     raise Http404
@@ -88,8 +98,13 @@ def clients(request, host):
   zks = ZooKeeperStats(host, port)
   clients = zks.get_clients()
 
-  return render('clients.mako', request,
-    dict(host=host, port=port, clients=clients))
+  return render('clients.mako', request, {
+    'clusters': CLUSTERS.get(),
+    'cluster': cluster,
+    'host': host,
+    'port': port,
+    'clients': clients
+  })
 
 
 def tree(request, id, path):
@@ -104,14 +119,17 @@ def tree(request, id, path):
 
 def delete(request, id, path):
   cluster = get_cluster_or_404(id)
+  redir = {}
   if request.method == 'POST':
     zk = ZooKeeper(cluster['rest_url'])
     try:
       zk.recursive_delete(path)
     except ZooKeeper.NotFound:
       pass
-
-  return tree(request, id, path[:path.rindex('/')] or '/')
+    redir = {
+      'redirect': reverse('zookeeper:tree', kwargs={'id':id, 'path': path[:path.rindex('/')] or '/'})
+    }
+  return HttpResponse(json.dumps(redir), mimetype="application/json")
 
 
 def create(request, id, path):
@@ -129,7 +147,7 @@ def create(request, id, path):
   else:
     form = CreateZNodeForm()
 
-  return render('create.mako', request, {'path': path, 'form': form, 'clusters': CLUSTERS.get(),})
+  return render('create.mako', request, {'cluster': cluster, 'path': path, 'form': form, 'clusters': CLUSTERS.get(),})
 
 
 def edit_as_base64(request, id, path):
@@ -150,7 +168,7 @@ def edit_as_base64(request, id, path):
       data=node.get('data64', ''),
       version=node.get('version', '-1')))
 
-  return render('edit.mako', request, {'path': path, 'form': form, 'clusters': CLUSTERS.get(),})
+  return render('edit.mako', request, {'cluster': cluster, 'path': path, 'form': form, 'clusters': CLUSTERS.get(),})
 
 
 def edit_as_text(request, id, path):
@@ -167,4 +185,4 @@ def edit_as_text(request, id, path):
   else:
     form = EditZNodeForm(dict(data=node.get('data64', '').decode('base64').strip(), version=node.get('version', '-1')))
 
-  return render('edit.mako', request, {'path': path, 'form': form, 'clusters': CLUSTERS.get(),})
+  return render('edit.mako', request, {'cluster': cluster, 'path': path, 'form': form, 'clusters': CLUSTERS.get(),})