Browse Source

HUE-8926 [frontend] Add new left navigation sidebar

Johan Ahlen 6 năm trước cách đây
mục cha
commit
81cf9c60a6

+ 1 - 1
desktop/core/src/desktop/js/hue.js

@@ -132,7 +132,7 @@ $(document).ready(() => {
   const sidebarViewModel = new SideBarViewModel(onePageViewModel, topNavViewModel);
   ko.applyBindings(sidebarViewModel, $('.hue-sidebar')[0]);
   if (window.IS_MULTICLUSTER_ONLY) {
-    ko.applyBindings(sidebarViewModel, $('.hue-dw-sidebar-container')[0]);
+    ko.applyBindings(sidebarViewModel, $('.hue-sidebar-container')[0]);
   }
 
   huePubSub.publish('cluster.config.get.config');

+ 0 - 146
desktop/core/src/desktop/js/ko/components/ko.dwSidebar.js

@@ -1,146 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import $ from 'jquery';
-import ko from 'knockout';
-
-import apiHelper from 'api/apiHelper';
-import componentUtils from './componentUtils';
-import huePubSub from 'utils/huePubSub';
-
-const TEMPLATE = `
-  <div class="hue-dw-brand" data-bind="css: { 'collapsed': collapsed }, toggle: leftNavVisible">
-    <img src="${window.STATIC_URLS['desktop/art/cloudera-data-warehouse-3.svg']}">
-  </div>
-
-  <div class="sidebar">
-    <div class="sidebar-content" data-bind="foreach: items">
-      <!-- ko if: $index() !== 0 -->
-      <h4 class="sidebar-category-item" data-bind="text: displayName"></h4>
-      <!-- /ko -->
-      <!-- ko foreach: children -->
-      <!-- ko if: $component.collapsed -->
-      <a role="button" class="sidebar-item" data-bind="hueLink: url, attr: { title: displayName }, css: { 'active': url === $component.activeUrl() }, tooltip: { placement: 'right' }, click: function() { if (url.startsWith('/jobbrowser')) { huePubSub.publish('context.selector.set.cluster', 'AltusV2'); } }">
-        <!-- ko ifnot: $component.collapsed -->
-        <span class="sidebar-caret"><i class="fa fa-caret-right"></i></span>
-        <!-- /ko -->
-        <span class="sidebar-icon with-tooltip"><!-- ko template: { name: 'app-icon-template' } --><!--/ko--></span>
-        <span class="sidebar-item-name" data-bind="text: displayName"></span>
-      </a>
-      <!-- /ko -->
-      <!-- ko ifnot: $component.collapsed -->
-      <a role="button" class="sidebar-item" data-bind="hueLink: url, attr: { title: displayName }, css: { 'active': url === $component.activeUrl() }, click: function() { if (url.startsWith('/jobbrowser')) { huePubSub.publish('context.selector.set.cluster', 'AltusV2'); } }">
-        <!-- ko ifnot: $component.collapsed -->
-        <span class="sidebar-caret"><i class="fa fa-caret-right"></i></span>
-        <!-- /ko -->
-        <span class="sidebar-icon without-tooltip"><!-- ko template: { name: 'app-icon-template' } --><!--/ko--></span>
-        <span class="sidebar-item-name" data-bind="text: displayName"></span>
-      </a>
-      <!-- /ko -->
-      <!-- /ko -->
-    </div>
-    <a class="sidebar-action-button" role="button" data-bind="toggle: collapsed">
-      <span aria-hidden="true" class="fa" data-bind="css: { 'fa-angle-double-right': collapsed, 'fa-angle-double-left': !collapsed() }"></span>
-    </a>
-  </div>
-`;
-
-class DwSidebar {
-  constructor(params, element) {
-    const self = this;
-    self.pocClusterMode = params.pocClusterMode;
-    // self.items =  params.items; // TODO: Once we have the proper apps in cluster config.
-
-    self.leftNavVisible = ko.observable(false);
-    self.leftNavVisible.subscribe(val => {
-      huePubSub.publish('hue.toggle.left.nav', val);
-    });
-
-    self.items = ko.pureComputed(() => {
-      const appCategory = {
-        displayName: 'Apps',
-        isCategory: true
-      };
-      if (self.pocClusterMode() === 'dw') {
-        appCategory.children = [
-          { displayName: 'Editor', url: '/editor/?type=impala', icon: 'editor' },
-          { displayName: 'Catalog', url: '/metastore/tables', icon: 'tables' },
-          { displayName: 'Importer', url: '/indexer/importer', icon: 'hdfs' },
-          { displayName: 'Warehouses', url: '/jobbrowser#!dataware2-clusters', icon: 'warehouses' },
-          { displayName: 'Dashboards', url: '/dashboard', icon: 'dashboard' }
-        ];
-      } else {
-        // DE mode
-        appCategory.children = [
-          { displayName: 'Editor', url: '/editor/?type=hive', icon: 'editor' },
-          { displayName: 'Dashboard', url: '/jobbrowser/#!workflows', icon: 'tables' },
-          { displayName: 'Workflows', url: '/oozie/editor/workflow/new', icon: 'workflows' },
-          {
-            displayName: 'Service',
-            url: '/hue/jobbrowser/#!dataware2-clusters',
-            icon: 'warehouses'
-          }
-        ];
-      }
-
-      return [appCategory];
-    });
-
-    self.collapsed = ko.observable();
-    self.collapsed.subscribe(newVal => {
-      if (newVal) {
-        $(element).addClass('collapsed');
-      } else {
-        $(element).removeClass('collapsed');
-      }
-    });
-    apiHelper.withTotalStorage('sidebar', 'collapsed', self.collapsed, false);
-
-    self.activeUrl = ko.observable();
-
-    // TODO: Figure out why it gets fired 30+ times on load
-    let throttle = -1;
-    huePubSub.subscribe('set.current.app.name', appName => {
-      window.clearTimeout(throttle);
-      if (!appName) {
-        return;
-      }
-      throttle = window.setTimeout(() => {
-        self.items().some(item => {
-          return item.children.some(child => {
-            if (location.href.indexOf(child.url) !== -1) {
-              if (self.activeUrl !== child.url) {
-                self.activeUrl(child.url);
-              }
-              return true;
-            }
-          });
-        });
-      }, 20);
-    });
-    huePubSub.publish('get.current.app.name');
-  }
-}
-
-componentUtils.registerComponent(
-  'hue-dw-sidebar',
-  {
-    createViewModel: function(params, componentInfo) {
-      return new DwSidebar(params, componentInfo.element);
-    }
-  },
-  TEMPLATE
-);

+ 239 - 0
desktop/core/src/desktop/js/ko/components/ko.sidebar.js

@@ -0,0 +1,239 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import componentUtils from 'ko/components/componentUtils';
+import huePubSub from 'utils/huePubSub';
+import I18n from 'utils/i18n';
+
+const TEMPLATE = `
+  <script type="text/html" id="sidebar-item">
+    <div class="item-wrapper"><a href="javascript: void(0);" data-bind="hueLink: item.url, attr: { 'aria-label': item.displayName, 'data-tooltip': item.displayName }, css: { 'active': item.active }" class="item">
+      <div class="icon" data-bind="template: 'app-icon-template'"></div><span data-bind="text: item.displayName"></span>
+    </a></div>
+  </script>
+
+  <div class="hue-sidebar-header"></div>
+  <div class="hue-sidebar-body">
+    <!-- ko foreach: {data: items, as: 'item'} -->
+      <!-- ko if: item.isCategory -->
+        <!-- ko ifnot: $index() === 0 -->
+        <div class="item-spacer"></div>
+        <!-- /ko -->
+        <!-- ko template: {name: 'sidebar-item', foreach: item.children, as: 'item'} --><!-- /ko -->
+      <!-- /ko -->
+      <!-- ko ifnot: item.isCategory -->
+        <!-- ko template: { name: 'sidebar-item' } --><!-- /ko -->
+      <!-- /ko -->
+    <!-- /ko -->
+  </div>
+  <div class="hue-sidebar-footer">
+    <a class="hue-sidebar-trigger" data-bind="toggle: collapsed">
+      <svg><use xlink:href="#hi-collapse-nav"></use></svg>
+    </a>
+  </div>
+`;
+
+class SidebarItem {
+  constructor(options) {
+    this.isCategory = !!options.isCategory;
+    this.displayName = options.displayName;
+    this.url = options.url;
+    this.icon = options.icon;
+    this.children = options.children;
+    this.name = options.name;
+
+    this.active = ko.observable(false);
+  }
+}
+
+class Sidebar {
+  constructor(params, element) {
+    this.$element = $(element);
+
+    this.collapsed = ko.observable();
+
+    this.collapsed.subscribe(newVal => {
+      if (newVal) {
+        this.$element.addClass('collapsed');
+      } else {
+        this.$element.removeClass('collapsed');
+      }
+    });
+
+    apiHelper.withTotalStorage('hue.sidebar', 'collabse', this.collapsed, true);
+
+    this.items = ko.observableArray();
+
+    huePubSub.subscribe('cluster.config.set.config', clusterConfig => {
+      const items = [];
+      if (clusterConfig && clusterConfig['app_config']) {
+        const appsItems = [];
+        const appConfig = clusterConfig['app_config'];
+        if (appConfig['editor']) {
+          let editor = null;
+          if (
+            clusterConfig['main_button_action'] &&
+            clusterConfig['main_button_action'].page.indexOf('/editor') === 0
+          ) {
+            editor = clusterConfig['main_button_action'];
+          }
+
+          if (!editor) {
+            const defaultEditor = appConfig['editor']['default_sql_interpreter'];
+            if (defaultEditor) {
+              const foundEditor = appConfig['editor']['interpreters'].filter(interpreter => {
+                return interpreter.type === defaultEditor;
+              });
+              if (foundEditor.length === 1) {
+                editor = foundEditor[0];
+              }
+            }
+          }
+
+          if (!editor && appConfig['editor']['interpreters'].length > 1) {
+            editor = appConfig['editor']['interpreters'][1];
+          }
+
+          if (editor) {
+            appsItems.push(
+              new SidebarItem({
+                displayName: I18n('Editor'),
+                url: editor['page'],
+                icon: 'editor'
+              })
+            );
+          } else {
+            appsItems.push(
+              new SidebarItem({
+                displayName: appConfig['editor']['displayName'],
+                url: appConfig['editor']['page'],
+                icon: 'editor'
+              })
+            );
+          }
+        }
+        ['dashboard', 'scheduler'].forEach(appName => {
+          if (appConfig[appName]) {
+            appsItems.push(
+              new SidebarItem({
+                displayName: appConfig[appName]['displayName'],
+                url: appConfig[appName]['page'],
+                icon: appName
+              })
+            );
+          }
+        });
+        if (appsItems.length > 0) {
+          items.push(
+            new SidebarItem({
+              isCategory: true,
+              displayName: I18n('Apps'),
+              children: appsItems
+            })
+          );
+        }
+
+        const browserItems = [];
+        browserItems.push(
+          new SidebarItem({
+            displayName: I18n('Documents'),
+            url: '/home/',
+            icon: 'documents'
+          })
+        );
+        if (appConfig['browser'] && appConfig['browser']['interpreters']) {
+          appConfig['browser']['interpreters'].forEach(browser => {
+            browserItems.push(
+              new SidebarItem({
+                displayName: browser.displayName,
+                url: browser.page,
+                icon: browser.type
+              })
+            );
+          });
+        }
+        if (browserItems.length > 0) {
+          items.push(
+            new SidebarItem({
+              isCategory: true,
+              displayName: I18n('Browsers'),
+              children: browserItems
+            })
+          );
+        }
+
+        const sdkItems = [];
+        if (appConfig['sdkapps'] && appConfig['sdkapps']['interpreters']) {
+          appConfig['sdkapps']['interpreters'].forEach(browser => {
+            sdkItems.push(
+              new SidebarItem({
+                displayName: browser['displayName'],
+                url: browser['page']
+              })
+            );
+          });
+        }
+        if (sdkItems.length > 0) {
+          items.push(
+            new SidebarItem({
+              isCategory: true,
+              displayName: appConfig['sdkapps']['displayName'],
+              children: sdkItems
+            })
+          );
+        }
+      }
+
+      this.items(items);
+    });
+
+    const updateActive = () => {
+      this.items().forEach(item => {
+        item.children.forEach(child => {
+          child.active(location.href.indexOf(child.url) !== -1)
+        });
+      });
+    };
+
+    let throttle = -1;
+    huePubSub.subscribe('set.current.app.name', appName => {
+      window.clearTimeout(throttle);
+      if (!appName) {
+        return;
+      }
+      throttle = window.setTimeout(updateActive, 20);
+    });
+    updateActive();
+  }
+
+  toggleCollapse() {
+    this.$element.toggleClass('collapsed');
+  }
+}
+
+componentUtils.registerComponent(
+  'hue-sidebar',
+  {
+    createViewModel: function(params, componentInfo) {
+      return new Sidebar(params, componentInfo.element);
+    }
+  },
+  TEMPLATE
+);

+ 1 - 1
desktop/core/src/desktop/js/ko/ko.all.js

@@ -146,7 +146,6 @@ import 'ko/components/ko.contextSelector';
 import 'ko/components/ko.createDirectoryModal';
 import 'ko/components/ko.deleteDocModal';
 import 'ko/components/ko.dropDown';
-import 'ko/components/ko.dwSidebar';
 import 'ko/components/ko.executionAnalysis';
 import 'ko/components/ko.favoriteApp';
 import 'ko/components/ko.fieldSamples';
@@ -160,6 +159,7 @@ import 'ko/components/ko.navTags';
 import 'ko/components/ko.performanceGraph';
 import 'ko/components/ko.pollingCatalogEntriesList';
 import 'ko/components/ko.sentryPrivileges';
+import 'ko/components/ko.sidebar';
 import 'ko/components/ko.sqlColumnsTable';
 
 import 'ko/extenders/ko.maxLength';

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue-embedded.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 0 - 217
desktop/core/src/desktop/static/desktop/less/components/hue-dw-sidebar.less

@@ -1,217 +0,0 @@
-/*
- Licensed to Cloudera, Inc. under one
- or more contributor license agreements.  See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership.  Cloudera, Inc. licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License.  You may obtain a copy of the License at
-
-     http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-@sidebar-width: 250px;
-@collapsed-sidebar-width: 48px;
-@sidebar-animation-duration: 250ms;
-@sidebar-category-font-size: 16px;
-@sidebar-left-spacing: 10px;
-
-.hue-dw-brand {
-  width: 247px;
-  height: 50px;
-  position: fixed;
-  left: 0;
-  top: 0;
-  background-color: #333d52;
-  z-index: 500000;
-  transition: width @sidebar-animation-duration;
-  overflow: hidden;
-
-  &.collapsed {
-    width: 46px;
-  }
-
-  img {
-    min-width: 191px;
-    max-width: 191px;
-    margin: 9px 9px;
-  }
-}
-
-.hue-dw-sidebar-container {
-  position: relative;
-  .flex(0 0);
-  .flex-basis(250px);
-
-  transition: flex-basis @sidebar-animation-duration;
-
-  .sidebar {
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 3px;
-    overflow: hidden;
-    background-color: #323543;
-    box-shadow: 0 0 3px 0 @cui-gray-600;
-
-    .fa::before {
-      font-size: 16px;
-      color: #b8bec4;
-    }
-
-    .sidebar-content {
-      top: 24px;
-      bottom: 36px;
-      position: absolute;
-      overflow-y: auto;
-      overflow-x: hidden;
-      width: 100%;
-    }
-
-    .sidebar-item {
-      display: block;
-      line-height: 36px;
-      color: #b8bec4;
-      white-space: nowrap;
-      font-size: 16px;
-
-      .sidebar-caret {
-        opacity: 0;
-        float: left;
-      }
-
-      .sidebar-icon {
-        float: left;
-        padding: 0 16px 0 14px;
-
-        &.with-tooltip {
-          display: none;
-        }
-      }
-
-      .fa,
-      .altus-icon {
-        width: 18px;
-        font-size: 16px;
-        text-align: center;
-        vertical-align: middle;
-      }
-    }
-
-    .sidebar-item.active {
-      background: #2d2f3b;
-      color: #5eab41;
-
-      .sidebar-caret {
-        opacity: 1;
-      }
-
-      .fa::before {
-        color: #5eab41;
-      }
-    }
-
-    .sidebar-item:not(.active):hover {
-      background-color: #404350;
-      color: @cui-white;
-
-      .fa::before {
-        background-color: #404350;
-        color: @cui-white;
-      }
-    }
-
-    .sidebar-item:active {
-      background: #2d2f3b;
-      color: #5eab41;
-    }
-
-    .sidebar-category-item {
-      margin: 20px 0 4px @sidebar-left-spacing;
-      font-size: 16px;
-      font-weight: bold;
-      color: @cui-gray-600;
-      transition: opacity @sidebar-animation-duration;
-      white-space: nowrap;
-    }
-
-    .sidebar-action-button {
-      cursor: pointer;
-      background-color: inherit;
-      display: block;
-      position: absolute;
-      bottom: 0;
-      right: 0;
-      left: 0;
-      line-height: 36px;
-      width: 100%;
-      text-align: right;
-      color: #b8bec4;
-      box-shadow: 0 0 3px 0 @cui-gray-600;
-
-      span {
-        padding-right: 16px;
-      }
-
-      &:hover {
-        background-color: #404350;
-        color: @cui-white;
-
-        .fa::before {
-          background-color: #404350;
-          color: @cui-white;
-        }
-      }
-    }
-
-    .sidebar-item-name {
-      transition: opacity @sidebar-animation-duration;
-      opacity: 1;
-      vertical-align: middle;
-    }
-  }
-
-  &.collapsed {
-    .flex-basis(49px);
-
-    .sidebar {
-      .sidebar-category-item,
-      .sidebar-item-name {
-        opacity: 0;
-      }
-
-      .sidebar-icon.without-tooltip {
-        display: none;
-      }
-
-      .sidebar-icon.with-tooltip {
-        display: initial;
-      }
-
-      .sidebar-action-button {
-        text-align: center;
-
-        span {
-          padding-right: 0;
-        }
-      }
-    }
-  }
-
-  .sidebar-placeholder {
-    transition: width @sidebar-animation-duration;
-    width: @sidebar-width;
-    height: 100%;
-    flex-shrink: 0;
-
-    &.collapsed {
-      width: 47px;
-    }
-  }
-}

+ 188 - 125
desktop/core/src/desktop/static/desktop/less/components/hue-sidebar.less

@@ -15,168 +15,231 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 */
+@sidebar-width: 248px;
+@collapsed-sidebar-width: 56px;
 
-@import (reference) "../cui/extra-variables.less";
-@import (reference) "../hue-mixins.less";
+@gradient-start: rgb(51, 62, 71);
+@gradient-end: rgb(2, 50, 89);
+@header-bg-open: rgb(28, 36, 41);
 
-@sidebar-width: 200px;
-@collapsed-sidebar-width: 48px;
-@sidebar-animation-duration: 250ms;
-@sidebar-category-font-size: 13px;
-@sidebar-left-spacing: 16px;
-@sidebar-left-spacing-item: 30px;
-@sidebar-background-color: @cui-gray-050;
+@item-color: rgb(173, 178, 182);
+@item-active-border: @hue-primary-color-dark;
+
+@header-bg: @hue-primary-color-dark;
 
 .hue-sidebar {
-  position: fixed;
-  top: @navbar-height;
-  left: 0;
-  bottom: 0;
-  background-color: @sidebar-background-color;
-  width: @sidebar-width;
-  transition: width @sidebar-animation-duration;
-  z-index: 5000;
+  .flex(0 0 @sidebar-width);
+  .display-flex;
+  .flex-direction(column);
+  .flex-shrink(0);
+
+  height: 100%;
+  background-image: linear-gradient(164deg, @gradient-start, @gradient-end);
+  line-height: 1.2;
   overflow: hidden;
-  box-shadow: 0 1px 2px 0;
+  transition: all 0.2s ease 0s;
 
-  &.hue-sidebar-below-top-bar {
-    position: absolute;
-    top: 0;
-    padding-top: 10px;
+  &:not(.collapsed) .hue-sidebar-header {
+    background-color: @header-bg-open !important;
   }
 
-  .hue-sidebar-content {
-    top: 24px;
-    bottom: 50px;
-    position: absolute;
-    overflow-y: auto;
-    min-width: @sidebar-width;
-  }
+  &.collapsed {
+    .flex-basis(@collapsed-sidebar-width);
 
-  .hue-sidebar-item {
-    display: block;
-    padding-left: @sidebar-left-spacing-item;
-    line-height: 38px;
-    color: @text-color;
-    background-color: @sidebar-background-color;
-
-    .hue-ease-transition(~"background-color, color");
-
-    .hue-sidebar-icon {
-      color: @cui-gray-600;
-      display: inline-block;
-      margin-right: 14px;
-      width: 18px;
-      text-align: center;
-      .hue-ease-transition(color);
+    .hue-sidebar-header {
+      padding: 0 12px;
     }
-  }
 
-  .hue-sidebar-item.active {
-    font-weight: bold;
-    color: @cui-blue-500;
-    background-color: @cui-gray-100;
+    .hue-sidebar-footer a.hue-sidebar-trigger svg {
+      transform: rotate(180deg);
+    }
+
+    .icon {
+      .flex-box-flex(0);
+      .flex(0 0 56px);
+
+      margin: 0;
+      font-size: 24px;
+      height: 24px;
 
-    .fa::before {
-      color: @cui-blue-500;
+      svg {
+        font-size: 24px;
+        height: 24px;
+      }
     }
-  }
 
-  .hue-sidebar-item:hover {
-    color: @cui-blue-500;
-    background-color: @cui-gray-100;
+    .item {
+      height: 56px;
+      padding: 0;
 
-    .sidebar-icon {
-      color: @cui-blue-500;
+      span {
+        opacity: 0;
+      }
+    }
+
+    .item[data-tooltip]:hover::before {
+      display: block;
+    }
+
+    .item[data-tooltip]::before {
+      z-index: 1010;
+      display: none;
+      content: attr(data-tooltip);
+      position: fixed;
+      left: 64px;
+      background-color: @gradient-start;
+      color: @cui-white;
+      font-size: 12px;
+      box-shadow: rgba(0, 0, 0, 0.2) 0 2px 4px 0;
+      white-space: nowrap;
+      border-radius: 3px;
+      padding: 4px 8px;
+    }
+
+    .hue-sidebar-header .hue-app-switcher-trigger svg {
+      width: 32px;
+      height: 32px;
     }
   }
 
-  .hue-sidebar-category-item {
-    margin: 20px 0 4px @sidebar-left-spacing;
-    font-size: @sidebar-category-font-size * 1.2;
-    color: @cui-blue-700;
-    transition: opacity @sidebar-animation-duration;
+  .item-spacer {
+    height: 15px;
   }
 
-  .hue-sidebar-action-button {
-    background-color: inherit;
-    display: block;
-    position: absolute;
-    bottom: 0;
-    right: 0;
-    left: 0;
-    line-height: 36px;
-    width: 100%;
-    text-align: right;
-    padding-right: 16px;
-    color: @text-color;
-    box-shadow: 0 0 2px 0;
-
-    &:hover {
-      background: @cui-gray-100;
+  .item {
+    .display-flex;
+    .flex-box-align(center);
+
+    height: 32px;
+    position: relative;
+    align-items: center;
+    color: @item-color;
+
+    span {
+      .nowrap-ellipsis;
+    }
+
+    &.active {
+      color: @cui-white;
+      background-color: rgba(255, 255, 255, 0.1);
+      font-weight: 700;
+
+      &::after {
+        content: "";
+        position: absolute;
+        right: 0;
+        top: 0;
+        bottom: 0;
+        width: 4px;
+        background-color: @item-active-border;
+      }
+
     }
   }
 
-  .hue-sidebar-item-name {
-    transition: opacity @sidebar-animation-duration;
-    opacity: 1;
+  a.item:hover:not(.active) {
+    color: @cui-white;
   }
 
-  .hue-sidebar-footer-panel {
-    background-color: inherit;
-    position: absolute;
-    bottom: 0;
-    right: 0;
-    left: 0;
-    width: 100%;
-    color: @text-color;
-    box-shadow: 0 0 2px 0;
-
-    .dropzone {
-      padding-top: 15px;
-      padding-bottom: 15px;
-      text-align: center;
-
-      i {
-        .hue-ease-transition(color);
-      }
+  .icon {
+    .display-flex;
+    .flex-box-flex(0);
+    .flex-box-pack(center);
+    .flex(0 0 36px);
+
+    margin-left: 16px;
+    text-align: center;
+    justify-content: center;
+    font-size: 20px;
+
+    svg {
+      font-size: 20px;
+      height: 20px; // nv d3 conflict
     }
+  }
 
-    .dz-drag-hover,
-    .dropzone:hover {
-      background-color: @cui-gray-200 !important;
+  * {
+    -webkit-font-smoothing: antialiased;
+    transition: all 0.2s ease 0s;
+    box-sizing: border-box;
 
-      i {
-        color: @cui-blue-500;
+    font-size: 100%;
+    vertical-align: baseline;
+    margin: 0;
+    padding: 0;
+    border-width: 0;
+    border-style: initial;
+    border-color: initial;
+    border-image: initial;
+  }
+
+  a {
+    cursor: pointer;
+    text-decoration: none;
+  }
+
+  .hue-sidebar-header {
+    .display-flex;
+    .flex-shrink(0);
+    .flex-box-align(center);
+
+    height: 56px;
+    align-items: center;
+    color: @cui-white;
+    font-size: 16px;
+    margin-bottom: 8px;
+    padding: 0 0 0 24px;
+    background-color: @header-bg;
+
+    .hue-app-switcher-trigger {
+      color: @hue-primary-color-dark;
+      margin-right: 6px;
+
+      svg {
+        width: 20px;
+        height: 20px;
       }
     }
 
-    a {
-      .pointer;
+    .logo {
+      height: 24px;
     }
-  }
-}
 
-.hue-sidebar.collapsed {
-  width: @collapsed-sidebar-width;
+    > a {
+      .display-flex;
+      .flex-box-align(center);
+      .flex-box-pack(center);
 
-  .hue-sidebar-category-item,
-  .sidebar-item-name {
-    opacity: 0;
+      align-items: center;
+      justify-content: center;
+    }
   }
 
-  .hue-sidebar-action-button {
-    text-align: center;
-    padding: 0;
-  }
-}
+  .hue-sidebar-body {
+    .flex(1 1 auto);
+    .flex-box-flex(1);
 
-.hue-sidebar-placeholder {
-  transition: width @sidebar-animation-duration;
-  width: @sidebar-width;
-  height: 100%;
+    overflow-y: hidden;
+  }
 
-  &.collapsed {
-    width: @collapsed-sidebar-width;
+  .hue-sidebar-footer {
+    a.hue-sidebar-trigger {
+      .display-flex;
+      .flex-shrink(0);
+      .flex-box-align(center);
+      .flex-box-pack(end);
+
+      align-items: center;
+      justify-content: flex-end;
+      margin-top: 12px;
+      height: 52px;
+      padding: 0 16px;
+
+      svg {
+        transition-delay: 0.2s;
+        width: 24px;
+        height: 24px;
+      }
+    }
   }
 }

+ 1 - 1
desktop/core/src/desktop/static/desktop/less/hue-cross-version.less

@@ -20,7 +20,7 @@
 @import "components/hue-breadcrumbs.less";
 @import "components/hue-catalog-entries-table.less";
 @import "components/hue-demi-modal.less";
-@import "components/hue-dw-sidebar.less";
+@import "components/hue-sidebar.less";
 @import "components/hue-execution-analysis.less";
 @import "components/hue-filechooser.less";
 @import "components/hue-gridster.less";

+ 24 - 0
desktop/core/src/desktop/static/desktop/less/hue-mixins.less

@@ -148,6 +148,30 @@
   flex-basis: @basis;
 }
 
+.flex-box-flex(@flex) {
+  -webkit-box-flex: @flex;
+  -moz-box-flex: @flex;
+  box-flex: @flex;
+}
+
+.flex-box-align(@align) {
+  -webkit-box-align: @align;
+  -moz-box-align: @align;
+  box-align: @align;
+}
+
+.flex-box-pack(@pack) {
+  -webkit-box-pack: @pack;
+  -moz-box-pack: @pack;
+  box-pack: @pack;
+}
+
+.flex-shrink(@shrink) {
+  -webkit-flex-shrink: @shrink;
+  -ms-flex-shrink: @shrink;
+  flex-shrink: @shrink;
+}
+
 .flex-direction(@direction: row) {
   -webkit-flex-direction: @direction;
   -ms-flex-direction: @direction;

+ 0 - 3
desktop/core/src/desktop/static/desktop/less/hue.less

@@ -23,9 +23,6 @@
 @import "hue-cross-version.less";
 @import "hue-multi-cluster-icons.less";
 @import "components/hue-clearable.less";
-@import "components/hue-sidebar.less";
-@import "components/hue-hamburger.less";
-@import "components/hue-multi-cluster-sidebar.less";
 @import "hue4.less";
 
 html {

+ 16 - 1
desktop/core/src/desktop/static/desktop/less/hue4.less

@@ -18,7 +18,20 @@
 @import (reference) "cui/colors.less";
 @import (reference) "hue-mixins.less";
 
+.hue-page {
+  position: relative;
+  .display-flex;
+  .flex-direction(row);
+
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
 .main-page {
+  .flex(1 1 auto);
   position: relative;
   .display-flex;
   .flex-direction(column);
@@ -60,16 +73,18 @@
 
   position: relative;
   z-index: 1009;
+  min-height: 54px;
 }
 
 .top-nav-left {
+  padding-top: 3px;
   .flex(0 0 340px);
 }
 
 .top-nav-middle {
   .flex(1);
 
-  padding: 0 10px;
+  padding: 3px 10px;
   width: 100%;
 }
 

+ 211 - 251
desktop/core/src/desktop/templates/hue.mako

@@ -181,286 +181,246 @@ ${ hueIcons.symbols() }
 <input style="display:none" readonly autocomplete="false" type="text" name="fakeusernameremembered"/>
 <input style="display:none" readonly autocomplete="false" type="password" name="fakepasswordremembered"/>
 
-<div class="main-page">
-  % if banner_message or conf.CUSTOM.BANNER_TOP_HTML.get():
-    <div class="banner">
-      ${ banner_message or conf.CUSTOM.BANNER_TOP_HTML.get() | n,unicode }
-    </div>
-  % endif
+<div class="hue-page">
+  <div class="hue-sidebar" data-bind="component: { name: 'hue-sidebar', params: {  } }"></div>
 
-  % if not IS_EMBEDDED.get():
-  <nav class="navbar navbar-default">
-    <div class="navbar-inner top-nav">
-      <div class="top-nav-left">
-        % if not IS_EMBEDDED.get():
-          % if not (IS_MULTICLUSTER_ONLY.get() and get_cluster_config(user)['has_computes']):
-          <a class="hamburger hamburger-hue pull-left" data-bind="toggle: leftNavVisible, css: { 'is-active': leftNavVisible }">
-            <span class="hamburger-box"><span class="hamburger-inner"></span></span>
-          </a>
-
-          <a class="brand" data-bind="hueLink: '/home/'" href="javascript: void(0);" title="${_('Documents')}">
-            <svg style="height: 24px; width: 120px;"><use xlink:href="#hi-logo"></use></svg>
-          </a>
-          % endif
-        % endif
+  <div class="main-page">
+    % if banner_message or conf.CUSTOM.BANNER_TOP_HTML.get():
+      <div class="banner">
+        ${ banner_message or conf.CUSTOM.BANNER_TOP_HTML.get() | n,unicode }
+      </div>
+    % endif
 
-        % if not IS_MULTICLUSTER_ONLY.get():
-        <div class="btn-group" data-bind="visible: true" style="display:none; margin-top: 8px">
-          <!-- ko if: mainQuickCreateAction -->
-          <!-- ko with: mainQuickCreateAction -->
-          <a class="btn btn-primary disable-feedback hue-main-create-btn" data-bind="hueLink: url, attr: {title: tooltip}, style: { borderBottomRightRadius: $parent.quickCreateActions().length > 1 ? '0px' : '4px', borderTopRightRadius: $parent.quickCreateActions().length > 1 ? '0px' : '4px' }">
-            <span data-bind="text: displayName"></span>
-          </a>
-          <!-- /ko -->
-          <!-- /ko -->
-          <button class="btn btn-primary dropdown-toggle hue-main-create-btn-dropdown" data-toggle="dropdown" data-bind="visible: quickCreateActions().length > 1 || (quickCreateActions().length == 1 && quickCreateActions()[0].children && quickCreateActions()[0].children.length > 1)">
-            <!-- ko ifnot: mainQuickCreateAction -->${ _('More') } <!-- /ko -->
-            <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu hue-main-create-dropdown" data-bind="foreach: { data: quickCreateActions, as: 'item' }">
-            <!-- ko template: 'quick-create-item-template' --><!-- /ko -->
-          </ul>
-        </div>
-        % endif
+    % if not IS_EMBEDDED.get():
+    <nav class="navbar navbar-default">
+      <div class="navbar-inner top-nav">
+        <div class="top-nav-left">
+          % if not IS_EMBEDDED.get():
+            % if not (IS_MULTICLUSTER_ONLY.get() and get_cluster_config(user)['has_computes']):
+            <a class="hamburger hamburger-hue pull-left" data-bind="toggle: leftNavVisible, css: { 'is-active': leftNavVisible }">
+              <span class="hamburger-box"><span class="hamburger-inner"></span></span>
+            </a>
+
+            <a class="brand" data-bind="hueLink: '/home/'" href="javascript: void(0);" title="${_('Documents')}">
+              <svg style="height: 24px; width: 120px;"><use xlink:href="#hi-logo"></use></svg>
+            </a>
+            % endif
+          % endif
 
-        <script type="text/html" id="quick-create-item-template">
-          <!-- ko if: item.dividerAbove -->
-          <li class="divider"></li>
-          <!-- /ko -->
-          <li data-bind="css: { 'dropdown-submenu': item.isCategory && item.children.length > 1 }">
-            <!-- ko if: item.url -->
-              <a href="javascript: void(0);" data-bind="hueLink: item.url">
-                <!-- ko if: item.icon -->
-                <!-- ko template: { name: 'app-icon-template', data: item } --><!-- /ko -->
-                <!-- /ko -->
-                <span data-bind="css: { 'dropdown-no-icon': !item.icon }, text: item.displayName"></span>
-              </a>
+          % if not IS_MULTICLUSTER_ONLY.get():
+          <div class="btn-group" data-bind="visible: true" style="display:none; margin-top: 8px">
+            <!-- ko if: mainQuickCreateAction -->
+            <!-- ko with: mainQuickCreateAction -->
+            <a class="btn btn-primary disable-feedback hue-main-create-btn" data-bind="hueLink: url, attr: {title: tooltip}, style: { borderBottomRightRadius: $parent.quickCreateActions().length > 1 ? '0px' : '4px', borderTopRightRadius: $parent.quickCreateActions().length > 1 ? '0px' : '4px' }">
+              <span data-bind="text: displayName"></span>
+            </a>
             <!-- /ko -->
-            <!-- ko if: item.href -->
-              <a data-bind="attr: { href: item.href }, text: item.displayName" target="_blank"></a>
             <!-- /ko -->
-            <!-- ko if: item.isCategory && item.children.length > 1 -->
-            <ul class="dropdown-menu" data-bind="foreach: { data: item.children, as: 'item' }">
+            <button class="btn btn-primary dropdown-toggle hue-main-create-btn-dropdown" data-toggle="dropdown" data-bind="visible: quickCreateActions().length > 1 || (quickCreateActions().length == 1 && quickCreateActions()[0].children && quickCreateActions()[0].children.length > 1)">
+              <!-- ko ifnot: mainQuickCreateAction -->${ _('More') } <!-- /ko -->
+              <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu hue-main-create-dropdown" data-bind="foreach: { data: quickCreateActions, as: 'item' }">
               <!-- ko template: 'quick-create-item-template' --><!-- /ko -->
             </ul>
-            <!-- /ko -->
-          </li>
-        </script>
-      </div>
-
-
-      <div class="top-nav-middle">
-        <div class="search-container-top" data-bind="component: 'hue-global-search'"></div>
-      </div>
-
-      <div class="top-nav-right">
-        % if user.is_authenticated() and section != 'login' and (cluster != ANALYTIC_DB or IS_MULTICLUSTER_ONLY.get()):
-        <div class="dropdown navbar-dropdown pull-right">
-          % if IS_MULTICLUSTER_ONLY.get():
-            ##<!-- ko component: { name: 'hue-app-switcher', params: { onPrem: ko.observable(false) } } --><!-- /ko -->
+          </div>
           % endif
 
-          <%
-            view_profile = user.has_hue_permission(action="access_view:useradmin:edit_user", app="useradmin") or is_admin(user)
-          %>
-          <button class="btn btn-flat" data-toggle="dropdown" data-bind="click: function(){ huePubSub.publish('hide.jobs.panel'); huePubSub.publish('hide.history.panel'); }">
-            <i class="fa fa-user"></i> ${ user.username } <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu">
-            % if view_profile:
-            <li><a href="javascript:void(0)" data-bind="hueLink: '/useradmin/users/edit/${ user.username }'" title="${ _('View Profile') if is_ldap_setup else _('Edit Profile') }"><i class="fa fa-fw fa-user"></i> ${_('My Profile')}</a></li>
-            % endif
-            % if is_admin(user):
-            <li data-bind="hueLink: '/useradmin/users/'"><a href="javascript: void(0);"><i class="fa fa-fw fa-group"></i> ${_('Manage Users')}</a></li>
-            % endif
-            % if is_admin(user):
-            <li><a data-bind="hueLink: '/about/'" href="javascript: void(0);"><span class="dropdown-no-icon">${_('Administration')}</span></a></li>
-            % endif
-            <li><a href="javascript:void(0)" onclick="huePubSub.publish('show.welcome.tour')"><span class="dropdown-no-icon">${_('Welcome Tour')}</span></a></li>
-            <li><a href="http://gethue.com" target="_blank"><span class="dropdown-no-icon">${_('Help')}</span></a></li>
+          <script type="text/html" id="quick-create-item-template">
+            <!-- ko if: item.dividerAbove -->
             <li class="divider"></li>
-            <li><a title="${_('Sign out')}" data-bind="hueLink: '/accounts/logout'" href="javascript: void(0);"><i class="fa fa-fw fa-sign-out"></i> ${ _('Sign out') }</a></li>
-          </ul>
+            <!-- /ko -->
+            <li data-bind="css: { 'dropdown-submenu': item.isCategory && item.children.length > 1 }">
+              <!-- ko if: item.url -->
+                <a href="javascript: void(0);" data-bind="hueLink: item.url">
+                  <!-- ko if: item.icon -->
+                  <!-- ko template: { name: 'app-icon-template', data: item } --><!-- /ko -->
+                  <!-- /ko -->
+                  <span data-bind="css: { 'dropdown-no-icon': !item.icon }, text: item.displayName"></span>
+                </a>
+              <!-- /ko -->
+              <!-- ko if: item.href -->
+                <a data-bind="attr: { href: item.href }, text: item.displayName" target="_blank"></a>
+              <!-- /ko -->
+              <!-- ko if: item.isCategory && item.children.length > 1 -->
+              <ul class="dropdown-menu" data-bind="foreach: { data: item.children, as: 'item' }">
+                <!-- ko template: 'quick-create-item-template' --><!-- /ko -->
+              </ul>
+              <!-- /ko -->
+            </li>
+          </script>
         </div>
-        % endif
-
-        % if has_multi_clusters():
-          <select data-bind="options: clusters, optionsText: 'name', value: 'id'" class="input-small" style="margin-top:8px">
-          </select>
-        % endif
-        <!-- ko component: 'hue-history-panel' --><!-- /ko -->
-        <!-- ko if: hasJobBrowser -->
-          <!-- ko component: { name: 'hue-job-browser-links', params: { onePageViewModel: onePageViewModel }} --><!-- /ko -->
-        <!-- /ko -->
-      </div>
 
-    </div>
-  </nav>
-
-  <div id="jobsPanel" class="jobs-panel" style="display: none;">
-    <div style="position: absolute; right: 0px; padding: 5px 10px;">
-      <a class="pointer inactive-action pull-right" onclick="huePubSub.publish('hide.jobs.panel')"><i class="fa fa-fw fa-times"></i></a>
-      <a class="pointer inactive-action pull-right" onclick="huePubSub.publish('mini.jb.expand'); huePubSub.publish('hide.jobs.panel')" title="${ _('Open in Job Browser') }">
-        <i class="fa fa-fw fa-expand"></i>
-      </a>
-    </div>
-    <div id="mini_jobbrowser"></div>
-  </div>
-  % endif
 
-  <div class="content-wrapper">
-
-    <script type="text/html" id="hue-tmpl-sidebar-link">
-      <a role="button" class="hue-sidebar-item" data-bind="hueLink: item.url, attr: { title: item.displayName }">
-        <span class="hue-sidebar-item-name" data-bind="text: item.displayName"></span>
-      </a>
-    </script>
-
-    <div class="hue-sidebar hue-sidebar-below-top-bar" data-bind="visible: leftNavVisible" style="display:none;">
-      % if IS_MULTICLUSTER_ONLY.get():
-        <!-- ko component: { name: 'hue-multi-cluster-sidebar' } --><!-- /ko -->
-      % else:
-        <div class="hue-sidebar-content">
-          <!-- ko foreach: {data: items, as: 'item'} -->
-          <!-- ko if: item.isCategory -->
-            <h4 class="hue-sidebar-category-item" data-bind="text: item.displayName"></h4>
-            <!-- ko template: {name: 'hue-tmpl-sidebar-link', foreach: item.children, as: 'item'} --><!-- /ko -->
-          <!-- /ko -->
-          <!-- ko ifnot: item.isCategory -->
-            <!-- ko template: { name: 'hue-tmpl-sidebar-link' } --><!-- /ko -->
-          <!-- /ko -->
-          <!-- /ko -->
+        <div class="top-nav-middle">
+          <div class="search-container-top" data-bind="component: 'hue-global-search'"></div>
         </div>
-        <div class="hue-sidebar-footer-panel">
-          % if hasattr(SHOW_UPLOAD_BUTTON, 'get') and SHOW_UPLOAD_BUTTON.get():
-          <div data-bind="dropzone: {
-            clickable: false,
-            url: '/filebrowser/upload/file?dest=' + DROPZONE_HOME_DIR,
-            params: { dest: DROPZONE_HOME_DIR },
-            paramName: 'hdfs_file',
-            onError: onePageViewModel.dropzoneError,
-            onComplete: onePageViewModel.dropzoneComplete },
-            click: function(){ page('/indexer/importer/') }" class="pointer" title="${ _('Import data wizard') }">
-            <div class="dz-message" data-dz-message><i class="fa fa-fw fa-cloud-upload"></i> ${ _('Click or Drop files here') }</div>
+
+        <div class="top-nav-right">
+          % if user.is_authenticated() and section != 'login' and (cluster != ANALYTIC_DB or IS_MULTICLUSTER_ONLY.get()):
+          <div class="dropdown navbar-dropdown pull-right">
+            % if IS_MULTICLUSTER_ONLY.get():
+              ##<!-- ko component: { name: 'hue-app-switcher', params: { onPrem: ko.observable(false) } } --><!-- /ko -->
+            % endif
+
+            <%
+              view_profile = user.has_hue_permission(action="access_view:useradmin:edit_user", app="useradmin") or is_admin(user)
+            %>
+            <button class="btn btn-flat" data-toggle="dropdown" data-bind="click: function(){ huePubSub.publish('hide.jobs.panel'); huePubSub.publish('hide.history.panel'); }">
+              <i class="fa fa-user"></i> ${ user.username } <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu">
+              % if view_profile:
+              <li><a href="javascript:void(0)" data-bind="hueLink: '/useradmin/users/edit/${ user.username }'" title="${ _('View Profile') if is_ldap_setup else _('Edit Profile') }"><i class="fa fa-fw fa-user"></i> ${_('My Profile')}</a></li>
+              % endif
+              % if is_admin(user):
+              <li data-bind="hueLink: '/useradmin/users/'"><a href="javascript: void(0);"><i class="fa fa-fw fa-group"></i> ${_('Manage Users')}</a></li>
+              % endif
+              % if is_admin(user):
+              <li><a data-bind="hueLink: '/about/'" href="javascript: void(0);"><span class="dropdown-no-icon">${_('Administration')}</span></a></li>
+              % endif
+              <li><a href="javascript:void(0)" onclick="huePubSub.publish('show.welcome.tour')"><span class="dropdown-no-icon">${_('Welcome Tour')}</span></a></li>
+              <li><a href="http://gethue.com" target="_blank"><span class="dropdown-no-icon">${_('Help')}</span></a></li>
+              <li class="divider"></li>
+              <li><a title="${_('Sign out')}" data-bind="hueLink: '/accounts/logout'" href="javascript: void(0);"><i class="fa fa-fw fa-sign-out"></i> ${ _('Sign out') }</a></li>
+            </ul>
           </div>
           % endif
+
+          % if has_multi_clusters():
+            <select data-bind="options: clusters, optionsText: 'name', value: 'id'" class="input-small" style="margin-top:8px">
+            </select>
+          % endif
+          <!-- ko component: 'hue-history-panel' --><!-- /ko -->
+          <!-- ko if: hasJobBrowser -->
+            <!-- ko component: { name: 'hue-job-browser-links', params: { onePageViewModel: onePageViewModel }} --><!-- /ko -->
+          <!-- /ko -->
         </div>
-      % endif
-    </div>
 
-    % if IS_MULTICLUSTER_ONLY.get():
-    <div class="hue-dw-sidebar-container collapsed" data-bind="component: { name: 'hue-dw-sidebar', params: { items: items, pocClusterMode: pocClusterMode } }"></div>
+      </div>
+    </nav>
+
+    <div id="jobsPanel" class="jobs-panel" style="display: none;">
+      <div style="position: absolute; right: 0px; padding: 5px 10px;">
+        <a class="pointer inactive-action pull-right" onclick="huePubSub.publish('hide.jobs.panel')"><i class="fa fa-fw fa-times"></i></a>
+        <a class="pointer inactive-action pull-right" onclick="huePubSub.publish('mini.jb.expand'); huePubSub.publish('hide.jobs.panel')" title="${ _('Open in Job Browser') }">
+          <i class="fa fa-fw fa-expand"></i>
+        </a>
+      </div>
+      <div id="mini_jobbrowser"></div>
+    </div>
     % endif
 
-    <div class="left-panel" data-bind="css: { 'side-panel-closed': !leftAssistVisible() }, visibleOnHover: { selector: '.hide-left-side-panel' }">
-      <a href="javascript:void(0);" style="z-index: 1002; display: none;" title="${_('Show Assist')}" class="pointer side-panel-toggle show-left-side-panel" data-bind="visible: !leftAssistVisible(), toggle: leftAssistVisible"><i class="fa fa-chevron-right"></i></a>
-      <a href="javascript:void(0);" style="display: none; opacity: 0;" title="${_('Hide Assist')}" class="pointer side-panel-toggle hide-left-side-panel" data-bind="visible: leftAssistVisible, toggle: leftAssistVisible"><i class="fa fa-chevron-left"></i></a>
-      <div class="assist" data-bind="component: {
-          name: 'assist-panel',
+    <div class="content-wrapper">
+      <div class="left-panel" data-bind="css: { 'side-panel-closed': !leftAssistVisible() }, visibleOnHover: { selector: '.hide-left-side-panel' }">
+        <a href="javascript:void(0);" style="z-index: 1002; display: none;" title="${_('Show Assist')}" class="pointer side-panel-toggle show-left-side-panel" data-bind="visible: !leftAssistVisible(), toggle: leftAssistVisible"><i class="fa fa-chevron-right"></i></a>
+        <a href="javascript:void(0);" style="display: none; opacity: 0;" title="${_('Hide Assist')}" class="pointer side-panel-toggle hide-left-side-panel" data-bind="visible: leftAssistVisible, toggle: leftAssistVisible"><i class="fa fa-chevron-left"></i></a>
+        <div class="assist" data-bind="component: {
+            name: 'assist-panel',
+            params: {
+              user: '${user.username}',
+              sql: {
+                navigationSettings: {
+                  openItem: false,
+                  showStats: true
+                }
+              },
+              visibleAssistPanels: ['sql']
+            }
+          }, visible: leftAssistVisible" style="display:none;"></div>
+      </div>
+
+      <div id="leftResizer" class="resizer" data-bind="visible: leftAssistVisible(), splitFlexDraggable : {
+        containerSelector: '.content-wrapper',
+        sidePanelSelector: '.left-panel',
+        sidePanelVisible: leftAssistVisible,
+        orientation: 'left',
+        onPosition: function() { huePubSub.publish('split.draggable.position') }
+      }"><div class="resize-bar"></div></div>
+
+
+      <div class="page-content">
+        <!-- ko hueSpinner: { spin: isLoadingEmbeddable, center: true, size: 'xlarge', blackout: true } --><!-- /ko -->
+        <div id="embeddable_editor" class="embeddable"></div>
+        <div id="embeddable_notebook" class="embeddable"></div>
+        <div id="embeddable_metastore" class="embeddable"></div>
+        <div id="embeddable_dashboard" class="embeddable"></div>
+        <div id="embeddable_oozie_workflow" class="embeddable"></div>
+        <div id="embeddable_oozie_coordinator" class="embeddable"></div>
+        <div id="embeddable_oozie_bundle" class="embeddable"></div>
+        <div id="embeddable_oozie_info" class="embeddable"></div>
+        <div id="embeddable_jobbrowser" class="embeddable"></div>
+        <div id="embeddable_filebrowser" class="embeddable"></div>
+        <div id="embeddable_home" class="embeddable"></div>
+        <div id="embeddable_catalog" class="embeddable"></div>
+        <div id="embeddable_indexer" class="embeddable"></div>
+        <div id="embeddable_kafka" class="embeddable"></div>
+        <div id="embeddable_importer" class="embeddable"></div>
+        <div id="embeddable_collections" class="embeddable"></div>
+        <div id="embeddable_indexes" class="embeddable"></div>
+        <div id="embeddable_useradmin_users" class="embeddable"></div>
+        <div id="embeddable_useradmin_groups" class="embeddable"></div>
+        <div id="embeddable_useradmin_newgroup" class="embeddable"></div>
+        <div id="embeddable_useradmin_editgroup" class="embeddable"></div>
+        <div id="embeddable_useradmin_permissions" class="embeddable"></div>
+        <div id="embeddable_useradmin_editpermission" class="embeddable"></div>
+        <div id="embeddable_useradmin_configurations" class="embeddable"></div>
+        <div id="embeddable_useradmin_newuser" class="embeddable"></div>
+        <div id="embeddable_useradmin_addldapusers" class="embeddable"></div>
+        <div id="embeddable_useradmin_addldapgroups" class="embeddable"></div>
+        <div id="embeddable_useradmin_edituser" class="embeddable"></div>
+        <div id="embeddable_hbase" class="embeddable"></div>
+        <div id="embeddable_security_hive" class="embeddable"></div>
+        <div id="embeddable_security_hdfs" class="embeddable"></div>
+        <div id="embeddable_security_hive2" class="embeddable"></div>
+        <div id="embeddable_security_solr" class="embeddable"></div>
+        <div id="embeddable_help" class="embeddable"></div>
+        <div id="embeddable_admin_wizard" class="embeddable"></div>
+        <div id="embeddable_logs" class="embeddable"></div>
+        <div id="embeddable_dump_config" class="embeddable"></div>
+        <div id="embeddable_threads" class="embeddable"></div>
+        <div id="embeddable_metrics" class="embeddable"></div>
+        <div id="embeddable_connectors" class="embeddable"></div>
+        <div id="embeddable_analytics" class="embeddable"></div>
+        <div id="embeddable_403" class="embeddable"></div>
+        <div id="embeddable_404" class="embeddable"></div>
+        <div id="embeddable_500" class="embeddable"></div>
+        <div id="embeddable_sqoop" class="embeddable"></div>
+        <div id="embeddable_jobsub" class="embeddable"></div>
+        % if other_apps:
+          % for other in other_apps:
+            <div id="embeddable_${ other.display_name }" class="embeddable"></div>
+          % endfor
+        % endif
+      </div>
+
+      <div class="right-panel" data-bind="css: { 'right-assist-minimized': !rightAssistVisible() }, visible: rightAssistAvailable, component: {
+          name: 'right-assist-panel',
           params: {
-            user: '${user.username}',
-            sql: {
-              navigationSettings: {
-                openItem: false,
-                showStats: true
-              }
-            },
-            visibleAssistPanels: ['sql']
+            visible: rightAssistVisible
           }
-        }, visible: leftAssistVisible" style="display:none;"></div>
-    </div>
-
-    <div id="leftResizer" class="resizer" data-bind="visible: leftAssistVisible(), splitFlexDraggable : {
-      containerSelector: '.content-wrapper',
-      sidePanelSelector: '.left-panel',
-      sidePanelVisible: leftAssistVisible,
-      orientation: 'left',
-      onPosition: function() { huePubSub.publish('split.draggable.position') }
-    }"><div class="resize-bar"></div></div>
-
-
-    <div class="page-content">
-      <!-- ko hueSpinner: { spin: isLoadingEmbeddable, center: true, size: 'xlarge', blackout: true } --><!-- /ko -->
-      <div id="embeddable_editor" class="embeddable"></div>
-      <div id="embeddable_notebook" class="embeddable"></div>
-      <div id="embeddable_metastore" class="embeddable"></div>
-      <div id="embeddable_dashboard" class="embeddable"></div>
-      <div id="embeddable_oozie_workflow" class="embeddable"></div>
-      <div id="embeddable_oozie_coordinator" class="embeddable"></div>
-      <div id="embeddable_oozie_bundle" class="embeddable"></div>
-      <div id="embeddable_oozie_info" class="embeddable"></div>
-      <div id="embeddable_jobbrowser" class="embeddable"></div>
-      <div id="embeddable_filebrowser" class="embeddable"></div>
-      <div id="embeddable_home" class="embeddable"></div>
-      <div id="embeddable_catalog" class="embeddable"></div>
-      <div id="embeddable_indexer" class="embeddable"></div>
-      <div id="embeddable_kafka" class="embeddable"></div>
-      <div id="embeddable_importer" class="embeddable"></div>
-      <div id="embeddable_collections" class="embeddable"></div>
-      <div id="embeddable_indexes" class="embeddable"></div>
-      <div id="embeddable_useradmin_users" class="embeddable"></div>
-      <div id="embeddable_useradmin_groups" class="embeddable"></div>
-      <div id="embeddable_useradmin_newgroup" class="embeddable"></div>
-      <div id="embeddable_useradmin_editgroup" class="embeddable"></div>
-      <div id="embeddable_useradmin_permissions" class="embeddable"></div>
-      <div id="embeddable_useradmin_editpermission" class="embeddable"></div>
-      <div id="embeddable_useradmin_configurations" class="embeddable"></div>
-      <div id="embeddable_useradmin_newuser" class="embeddable"></div>
-      <div id="embeddable_useradmin_addldapusers" class="embeddable"></div>
-      <div id="embeddable_useradmin_addldapgroups" class="embeddable"></div>
-      <div id="embeddable_useradmin_edituser" class="embeddable"></div>
-      <div id="embeddable_hbase" class="embeddable"></div>
-      <div id="embeddable_security_hive" class="embeddable"></div>
-      <div id="embeddable_security_hdfs" class="embeddable"></div>
-      <div id="embeddable_security_hive2" class="embeddable"></div>
-      <div id="embeddable_security_solr" class="embeddable"></div>
-      <div id="embeddable_help" class="embeddable"></div>
-      <div id="embeddable_admin_wizard" class="embeddable"></div>
-      <div id="embeddable_logs" class="embeddable"></div>
-      <div id="embeddable_dump_config" class="embeddable"></div>
-      <div id="embeddable_threads" class="embeddable"></div>
-      <div id="embeddable_metrics" class="embeddable"></div>
-      <div id="embeddable_connectors" class="embeddable"></div>
-      <div id="embeddable_analytics" class="embeddable"></div>
-      <div id="embeddable_403" class="embeddable"></div>
-      <div id="embeddable_404" class="embeddable"></div>
-      <div id="embeddable_500" class="embeddable"></div>
-      <div id="embeddable_sqoop" class="embeddable"></div>
-      <div id="embeddable_jobsub" class="embeddable"></div>
-      % if other_apps:
-        % for other in other_apps:
-          <div id="embeddable_${ other.display_name }" class="embeddable"></div>
-        % endfor
-      % endif
-    </div>
+        }" style="display: none;"></div>
+
+      <div class="context-panel" data-bind="slideVisible: contextPanelVisible">
+        <div class="margin-top-10 padding-left-10 padding-right-10">
+          <h4 class="margin-bottom-30"><i class="fa fa-cogs"></i> ${_('Session')}</h4>
+          <div class="context-panel-content">
+            <!-- ko if: sessionsAvailable() && templateApp() -->
+            <div class="row-fluid">
+              <div class="span11" data-bind="template: { name: 'notebook-session-config-template' + templateApp(), data: activeAppViewModel }"></div>
+            </div>
+            <!-- /ko -->
 
-    <div class="right-panel" data-bind="css: { 'right-assist-minimized': !rightAssistVisible() }, visible: rightAssistAvailable, component: {
-        name: 'right-assist-panel',
-        params: {
-          visible: rightAssistVisible
-        }
-      }" style="display: none;"></div>
-
-    <div class="context-panel" data-bind="slideVisible: contextPanelVisible">
-      <div class="margin-top-10 padding-left-10 padding-right-10">
-        <h4 class="margin-bottom-30"><i class="fa fa-cogs"></i> ${_('Session')}</h4>
-        <div class="context-panel-content">
-          <!-- ko if: sessionsAvailable() && templateApp() -->
-          <div class="row-fluid">
-            <div class="span11" data-bind="template: { name: 'notebook-session-config-template' + templateApp(), data: activeAppViewModel }"></div>
+            <!-- ko ifnot: sessionsAvailable() && templateApp() -->
+            ${_('There are currently no information about the sessions.')}
+            <!-- /ko -->
           </div>
-          <!-- /ko -->
-
-          <!-- ko ifnot: sessionsAvailable() && templateApp() -->
-          ${_('There are currently no information about the sessions.')}
-          <!-- /ko -->
         </div>
+        <a class="pointer demi-modal-chevron" style="position: absolute; bottom: 0" data-bind="click: function () { huePubSub.publish('context.panel.visible.editor', false); }"><i class="fa fa-chevron-up"></i></a>
       </div>
-      <a class="pointer demi-modal-chevron" style="position: absolute; bottom: 0" data-bind="click: function () { huePubSub.publish('context.panel.visible.editor', false); }"><i class="fa fa-chevron-up"></i></a>
     </div>
   </div>
 </div>
-
 ${ commonshare() | n,unicode }
 
 ${ render_bundle('vendors~hue~notebook~tableBrowser') | n,unicode }

+ 4 - 0
desktop/core/src/desktop/templates/hue_icons.mako

@@ -28,6 +28,10 @@
       <ellipse cx="320" cy="98.97" rx="178.78" ry="47.62"></ellipse>
     </symbol>
 
+    <symbol id="hi-collapse-nav" viewBox="0 0 24 24">
+      <path fill="#adb2b6" fill-rule="evenodd" d="M18.62 4l-7.903 7.956L18.615 20 20 18.516l-6.432-6.552 6.426-6.47L18.62 4zm-6.719 0L4 11.956 11.897 20l1.385-1.484-6.432-6.552 6.427-6.47L11.901 4z"></path>
+    </symbol>
+
     <symbol id="hi-dashboard" viewBox="0 0 640 640">
       <path d="M404.71,252.92c56-63.64,111.37-126.54,170-193-25.83,0-47-.37-68.13.19-14.23.37-25.27-3.56-26.58-19.65-1.12-12.92,9.36-20.4,27.52-20.4,35.19,0,70.38-.19,105.57,0,20.59.19,26.77,6,26.77,26.58.19,36.31-.19,72.81.19,109.13.19,14-5.43,24.33-19.84,24.33s-20.4-9.92-20.22-24.15c.19-19.47,0-38.93,0-63.27-8.61,9.36-14.6,15.54-20.22,22.09-50.54,57.28-100.89,114.74-151.43,171.83-15.91,18-22.84,18.53-41.93,3.37-50.91-40.43-101.64-81-153.49-122.6-4.87,4.31-9.73,8.24-14.41,12.73Q131.14,262.37,43.82,344.45c-4.31,4.12-8.42,8.42-13.29,12-9.55,6.93-21,7.11-26.58-2.81C.21,347.07,2.08,336.4,3.76,328c.94-4.31,7.11-7.67,11-11.42q97.71-92.09,195.42-184c17.41-16.28,22.65-16.47,41.18-1.68l137,109.5C393.66,244.49,398.9,248.42,404.71,252.92Z"></path>
       <rect y="440.45" width="119.71" height="179.57"></rect>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác