فهرست منبع

[frontend] Switch to the new sidebar

Johan Ahlen 4 سال پیش
والد
کامیت
03174efbe1
36فایلهای تغییر یافته به همراه2641 افزوده شده و 1877 حذف شده
  1. 2 18
      desktop/core/src/desktop/js/components/HueLink.vue
  2. 255 0
      desktop/core/src/desktop/js/components/sidebar/AccordionItem.vue
  3. 52 0
      desktop/core/src/desktop/js/components/sidebar/AccordionSubItem.vue
  4. 74 0
      desktop/core/src/desktop/js/components/sidebar/BaseNavigationItem.vue
  5. 38 0
      desktop/core/src/desktop/js/components/sidebar/BaseNavigationItemTooltip.vue
  6. 51 0
      desktop/core/src/desktop/js/components/sidebar/HelpDrawerContent.vue
  7. 478 0
      desktop/core/src/desktop/js/components/sidebar/HueSidebar.vue
  8. 5 13
      desktop/core/src/desktop/js/components/sidebar/HueSidebarWebComponent.ts
  9. 100 0
      desktop/core/src/desktop/js/components/sidebar/NavigationItem.vue
  10. 38 0
      desktop/core/src/desktop/js/components/sidebar/SectionItem.vue
  11. 177 0
      desktop/core/src/desktop/js/components/sidebar/Sidebar.vue
  12. 119 0
      desktop/core/src/desktop/js/components/sidebar/SidebarBody.vue
  13. 65 0
      desktop/core/src/desktop/js/components/sidebar/SidebarDrawer.vue
  14. 29 0
      desktop/core/src/desktop/js/components/sidebar/SpacerItem.vue
  15. 69 0
      desktop/core/src/desktop/js/components/sidebar/UserDrawerContent.vue
  16. 432 0
      desktop/core/src/desktop/js/components/sidebar/drawer.scss
  17. 53 0
      desktop/core/src/desktop/js/components/sidebar/hueSidebar.scss
  18. 470 0
      desktop/core/src/desktop/js/components/sidebar/sidebar.scss
  19. 65 0
      desktop/core/src/desktop/js/components/sidebar/types.ts
  20. 16 13
      desktop/core/src/desktop/js/components/sidebar/variables.scss
  21. 21 0
      desktop/core/src/desktop/js/components/styles/variables.scss
  22. 2 7
      desktop/core/src/desktop/js/hue.js
  23. 0 75
      desktop/core/src/desktop/js/ko/bindings/ko.hueAppIcon.js
  24. 0 38
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.multiClusterSidebar.test.js.snap
  25. 0 228
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.sidebar.test.js.snap
  26. 0 136
      desktop/core/src/desktop/js/ko/components/ko.multiClusterSidebar.js
  27. 0 557
      desktop/core/src/desktop/js/ko/components/ko.sidebar.js
  28. 0 3
      desktop/core/src/desktop/js/ko/ko.all.js
  29. 0 145
      desktop/core/src/desktop/js/sideBarViewModel.js
  30. 3 1
      desktop/core/src/desktop/js/types/config.ts
  31. 6 0
      desktop/core/src/desktop/js/types/types.ts
  32. 20 0
      desktop/core/src/desktop/js/utils/hueUtils.ts
  33. 0 127
      desktop/core/src/desktop/static/desktop/less/components/hue-multi-cluster-sidebar.less
  34. 0 514
      desktop/core/src/desktop/static/desktop/less/components/hue-sidebar.less
  35. 0 1
      desktop/core/src/desktop/static/desktop/less/hue-cross-version.less
  36. 1 1
      desktop/core/src/desktop/templates/hue.mako

+ 2 - 18
desktop/core/src/desktop/js/components/HueLink.vue

@@ -21,7 +21,7 @@
 </template>
 
 <script lang="ts">
-  import huePubSub from '../utils/huePubSub';
+  import { onHueLinkClick } from 'utils/hueUtils';
   import Vue from 'vue';
   import Component from 'vue-class-component';
   import { Prop } from 'vue-property-decorator';
@@ -41,23 +41,7 @@
 
     clicked(event: Event): void {
       if (this.url) {
-        if (this.url.indexOf('http') === 0) {
-          window.open(this.url, this.$attrs.target);
-        } else {
-          const prefix =
-            (<hueWindow>window).HUE_BASE_URL + '/hue' + (this.url.indexOf('/') === 0 ? '' : '/');
-          if (this.$attrs.target) {
-            window.open(prefix + this.url, this.$attrs.target);
-          } else if (
-            (<KeyboardEvent>event).ctrlKey ||
-            (<KeyboardEvent>event).metaKey ||
-            (<KeyboardEvent>event).which === 2
-          ) {
-            window.open(prefix + this.url, '_blank');
-          } else {
-            huePubSub.publish('open.link', this.url);
-          }
-        }
+        onHueLinkClick(event, this.url, this.$attrs.target);
       } else {
         this.$emit('click');
       }

+ 255 - 0
desktop/core/src/desktop/js/components/sidebar/AccordionItem.vue

@@ -0,0 +1,255 @@
+<!--
+  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
+  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.
+-->
+
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div
+    ref="containerRef"
+    class="sidebar-accordion-item"
+    :class="{ 'sidebar-active': isActive }"
+    @mouseenter="onMouseEnter"
+    @mouseleave="onMouseLeave"
+    @focusout="onFocusOut"
+  >
+    <button
+      class="sidebar-base-btn sidebar-sidebar-item"
+      :aria-label="item.displayName"
+      :class="{ 'sidebar-active': isActive, 'sidebar-accordion-item-btn-open': tooltip }"
+      @click="toggleOpen"
+      @keypress="onKeyPress"
+    >
+      <div
+        class="sidebar-accordion-collapse-icon"
+        :class="{ 'sidebar-accordion-collapse-icon-open': isOpen }"
+      >
+        <svg viewBox="0 0 24 24" width="1em" height="1em">
+          <path
+            d="M17 12l-8.678 8L7 18.515 14.069 12 7 5.484 8.322 4z"
+            fill="currentColor"
+            fill-rule="evenodd"
+          />
+        </svg>
+      </div>
+
+      <div v-if="item.iconHtml" class="sidebar-sidebar-item-icon" v-html="item.iconHtml" />
+
+      <span>{{ item.displayName }}</span>
+
+      <div style="flex-grow: 1;" />
+    </button>
+
+    <div
+      v-if="!isCollapsed"
+      ref="accordionItems"
+      class="sidebar-accordion-items"
+      :style="{ height: accordionItemsHeight }"
+      :aria-hidden="!isOpen || isCollapsed"
+    >
+      <AccordionSubItem
+        v-for="subItem in item.children"
+        :key="subItem.name"
+        :item="subItem"
+        :active="activeItemName === subItem.name"
+        :disabled="!isOpen || isCollapsed"
+      />
+    </div>
+
+    <div
+      v-if="tooltip && isCollapsed"
+      :style="{ top: tooltip.top + 'px', left: tooltip.right - 28 + 'px' }"
+      class="sidebar-sidebar-item-tooltip-extra-hover-area"
+      @click="toggleOpen"
+    />
+
+    <BaseNavigationItemTooltip
+      v-if="tooltip"
+      :visible="isCollapsed"
+      :style="{
+        top: tooltip.top + 'px',
+        left: tooltip.right + 'px',
+        'max-height': tooltip.maxHeight + 'px'
+      }"
+      role="tooltip"
+    >
+      <div
+        class="sidebar-sidebar-item-tooltip-primary"
+        :class="{ 'sidebar-active': isActive }"
+        :style="{ height: tooltip.height + 'px' }"
+      >
+        {{ item.displayName }}
+      </div>
+      <div
+        class="sidebar-sidebar-item-tooltip-accordion-items"
+        :class="{ 'sidebar-sidebar-item-tooltip-accordion-items-overflow-top': isTooltipScrolled }"
+        @scroll="onTooltipAccordionItemsScroll"
+      >
+        <AccordionSubItem
+          v-for="child in item.children"
+          :key="child.name"
+          :item="child"
+          :active="activeItemName === child.name"
+        />
+      </div>
+    </BaseNavigationItemTooltip>
+  </div>
+</template>
+<!-- eslint-enable vue/no-v-html -->
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Inject, Prop } from 'vue-property-decorator';
+  import AccordionSubItem from './AccordionSubItem.vue';
+  import BaseNavigationItemTooltip from './BaseNavigationItemTooltip.vue';
+  import { SidebarAccordionItem, SidebarNavigationItem } from './types';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  interface Tooltip {
+    top: number;
+    right: number;
+    height: number;
+    maxHeight: number;
+    fromKeyboard?: boolean;
+  }
+
+  @Component({
+    components: { BaseNavigationItemTooltip, AccordionSubItem }
+  })
+  export default class AccordionItem extends Vue {
+    @Inject()
+    selectedItemChanged?: (itemName: string) => void;
+
+    @Prop()
+    item!: SidebarAccordionItem;
+    @Prop()
+    isCollapsed!: boolean;
+    @Prop()
+    activeItemName?: string | null = null;
+
+    isOpen = false;
+    isTooltipScrolled = false;
+    tooltip: Tooltip | null = null;
+
+    subTracker = new SubscriptionTracker();
+
+    disposeParentScroll?: () => void;
+
+    get isActive(): boolean {
+      return (
+        this.item.name === this.activeItemName ||
+        this.item.children.some(item => (<SidebarNavigationItem>item).name === this.activeItemName)
+      );
+    }
+
+    get accordionItemsHeight(): string {
+      const el = <HTMLElement>this.$refs.accordionItems;
+      if (this.isOpen && el) {
+        return `${el.scrollHeight}px`;
+      }
+      return '0';
+    }
+
+    mounted(): void {
+      const containerEl = <HTMLElement>this.$refs.containerRef;
+      if (containerEl) {
+        const parent = containerEl.parentElement;
+        if (parent) {
+          this.subTracker.addEventListener(parent, 'scroll', () => {
+            this.tooltip = null;
+          });
+        }
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    onFocusOut(): void {
+      if (!this.tooltip) {
+        return;
+      }
+      window.setTimeout(() => {
+        const el = this.$refs.containerRef;
+        if (
+          el &&
+          this.tooltip &&
+          this.tooltip.fromKeyboard &&
+          !(<HTMLElement>el).contains(document.activeElement)
+        ) {
+          this.tooltip = null;
+        }
+      }, 10);
+    }
+
+    onKeyPress(event: KeyboardEvent): void {
+      if ((event.key === 'Enter' || event.key === ' ') && this.isCollapsed) {
+        if (!this.tooltip) {
+          this.openTooltip(event.target as HTMLElement, true);
+        } else {
+          this.tooltip = null;
+        }
+      }
+    }
+
+    onMouseEnter(event: MouseEvent): void {
+      if (this.isCollapsed) {
+        this.openTooltip(event.target as HTMLElement);
+      }
+    }
+
+    onMouseLeave(): void {
+      this.tooltip = null;
+    }
+
+    onTooltipAccordionItemsScroll(event: Event): void {
+      this.isTooltipScrolled = (event.target as HTMLElement).scrollTop > 0;
+    }
+
+    openTooltip(el: HTMLElement, fromKeyboard?: boolean): void {
+      const rect = el.getBoundingClientRect();
+      // maxHeight is needed for the edge-cases where the accordion tooltip content would
+      // otherwise render outside the viewport.
+      const maxHeight = window.innerHeight - rect.top;
+
+      this.isTooltipScrolled = false;
+      this.tooltip = {
+        top: rect.top,
+        right: rect.right,
+        height: rect.height,
+        maxHeight,
+        fromKeyboard
+      };
+    }
+
+    toggleOpen(event: MouseEvent): void {
+      if (!this.isCollapsed) {
+        this.isOpen = !this.isOpen;
+      } else if (this.item.handler) {
+        this.item.handler(event);
+        if (this.selectedItemChanged) {
+          const firstNavChild = (<SidebarNavigationItem[]>this.item.children).find(
+            item => item.name
+          );
+          this.selectedItemChanged((firstNavChild && firstNavChild.name) || this.item.name);
+        }
+      }
+    }
+  }
+</script>

+ 52 - 0
desktop/core/src/desktop/js/components/sidebar/AccordionSubItem.vue

@@ -0,0 +1,52 @@
+<!--
+  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.
+-->
+
+<template>
+  <BaseNavigationItem
+    :key="item.name"
+    :css-classes="
+      active
+        ? 'sidebar-sidebar-item-accordion-sub-item sidebar-sidebar-item-accordion-sub-item-active'
+        : 'sidebar-sidebar-item-accordion-sub-item'
+    "
+    :disabled="disabled"
+    :item="item"
+  >
+    {{ item.displayName }}
+  </BaseNavigationItem>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import BaseNavigationItem from './BaseNavigationItem.vue';
+  import { SidebarAccordionSubItem } from './types';
+
+  @Component({
+    components: { BaseNavigationItem }
+  })
+  export default class AccordionSubItem extends Vue {
+    @Prop()
+    item!: SidebarAccordionSubItem;
+    @Prop()
+    active!: boolean;
+    @Prop({ default: false })
+    disabled?: boolean;
+  }
+</script>

+ 74 - 0
desktop/core/src/desktop/js/components/sidebar/BaseNavigationItem.vue

@@ -0,0 +1,74 @@
+<!--
+  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.
+-->
+
+<template>
+  <Fragment>
+    <button
+      v-if="item.handler"
+      v-bind="$attrs"
+      :class="cssClasses"
+      class="sidebar-base-btn"
+      @click="onClick"
+    >
+      <slot />
+    </button>
+    <a
+      v-if="item.url && !item.handler"
+      :class="cssClasses"
+      :href="item.url"
+      v-bind="$attrs"
+      class="sidebar-base-link"
+      @click="onClick"
+    >
+      <slot />
+    </a>
+  </Fragment>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import { Fragment } from 'vue-fragment';
+  import Component from 'vue-class-component';
+  import { Inject, Prop } from 'vue-property-decorator';
+  import { SidebarNavigationItem } from './types';
+
+  @Component({
+    components: { Fragment }
+  })
+  export default class BaseNavigationItem extends Vue {
+    @Inject()
+    selectedItemChanged?: (itemName: string) => void;
+
+    @Prop()
+    item!: Pick<SidebarNavigationItem, 'url' | 'handler' | 'name'>;
+    @Prop({ default: false })
+    disabled?: boolean;
+    @Prop({ default: '' })
+    cssClasses?: string;
+
+    onClick(event: Event): void {
+      if (this.selectedItemChanged) {
+        this.selectedItemChanged(this.item.name);
+      }
+      if (this.item.handler) {
+        this.item.handler(event);
+      }
+      this.$emit('click');
+    }
+  }
+</script>

+ 38 - 0
desktop/core/src/desktop/js/components/sidebar/BaseNavigationItemTooltip.vue

@@ -0,0 +1,38 @@
+<!--
+  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.
+-->
+
+<template>
+  <div
+    class="sidebar-sidebar-item-tooltip"
+    :class="{ 'sidebar-sidebar-item-tooltip-visible': visible }"
+  >
+    <slot />
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+
+  @Component
+  export default class BaseNavigationItemTooltip extends Vue {
+    @Prop({ default: false })
+    visible?: boolean;
+  }
+</script>

+ 51 - 0
desktop/core/src/desktop/js/components/sidebar/HelpDrawerContent.vue

@@ -0,0 +1,51 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="sidebar-drawer-help-content">
+    <div class="sidebar-drawer-help-content-header">
+      <h2>Help</h2>
+    </div>
+    <ul>
+      <li v-for="childItem in children" :key="childItem.name">
+        <BaseNavigationItem :item="childItem" @click="hideDrawer">
+          {{ childItem.displayName }}
+        </BaseNavigationItem>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Inject, Prop } from 'vue-property-decorator';
+  import BaseNavigationItem from './BaseNavigationItem.vue';
+  import { SidebarAccordionSubItem } from './types';
+
+  @Component({
+    components: { BaseNavigationItem }
+  })
+  export default class SmallHelpDrawerContent extends Vue {
+    @Inject()
+    hideDrawer!: () => void;
+
+    @Prop({ required: false, default: [] })
+    children?: SidebarAccordionSubItem[];
+  }
+</script>

+ 478 - 0
desktop/core/src/desktop/js/components/sidebar/HueSidebar.vue

@@ -0,0 +1,478 @@
+<!--
+  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.
+-->
+
+<template>
+  <Sidebar
+    :sidebar-items="sidebarItems"
+    :user-drawer-item="userDrawerItem"
+    :user-drawer-children="userDrawerChildren"
+    :help-drawer-item="helpDrawerItem"
+    :help-drawer-children="helpDrawerChildren"
+    :active-item-name="activeItemName"
+    :is-collapsed="isCollapsed"
+    :drawer-topic="drawerTopic"
+    @toggle-collapsed="toggleCollapsed"
+  />
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Provide } from 'vue-property-decorator';
+  import './hueSidebar.scss';
+  import Sidebar from './Sidebar.vue';
+  import {
+    HelpDrawerItem,
+    SidebarAccordionItem,
+    SidebarAccordionSubItem,
+    SidebarItem,
+    SidebarNavigationItem,
+    UserDrawerItem
+  } from './types';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import {
+    ASSIST_ACTIVE_DB_CHANGED_EVENT,
+    ASSIST_SET_DATABASE_EVENT
+  } from 'ko/components/assist/events';
+  import { AppType, Connector, EditorInterpreter, HueConfig, Namespace } from 'types/config';
+  import { hueWindow } from 'types/types';
+  import { CONFIG_REFRESHED_EVENT, getLastKnownConfig } from 'utils/hueConfig';
+  import huePubSub from 'utils/huePubSub';
+  import { onHueLinkClick } from 'utils/hueUtils';
+  import I18n from 'utils/i18n';
+  import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
+
+  interface EditorDatabaseDetails {
+    connector: Connector;
+    namespace: Namespace;
+    name: string;
+  }
+
+  interface AssistDatabaseDetails {
+    connector: Connector;
+    namespace: Namespace;
+    database: string;
+  }
+
+  const APP_ICON_INDEX: { [name: string]: string } = {
+    abfs: `<svg class="hi hi-fw"><use xlink:href="#hi-adls"></use></svg>`,
+    adls: `<svg class="hi hi-fw"><use xlink:href="#hi-adls"></use></svg>`,
+    dashboard: `<svg class="hi hi-fw"><use xlink:href="#hi-dashboard"></use></svg>`,
+    default: `<i class="fa fa-fw fa-database"></i>`,
+    'dist-cp': `<i class="fa fa-fw fa-files-o"></i>`,
+    documents: `<svg class="hi hi-fw"><use xlink:href="#hi-documents"></use></svg>`,
+    editor: `<svg class="hi hi-fw"><use xlink:href="#hi-editor"></use></svg>`,
+    hbase: `<i class="fa fa-fw fa-th-large"></i>`,
+    hdfs: `<i class="fa fa-fw fa-files-o"></i>`,
+    hive: `<svg class="hi hi-fw"><use xlink:href="#hi-hive"></use></svg>`,
+    impala: `<svg class="hi hi-fw"><use xlink:href="#hi-impala"></use></svg>`,
+    importer: `<i class="fa fa-fw fa-cloud-upload"></i>`,
+    indexes: `<i class="fa fa-fw fa-search-plus"></i>`,
+    jar: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
+    java: `<i class="fa fa-fw fa-file-code-o"></i>`,
+    'job-designer': `<svg class="hi hi-fw"><use xlink:href="#hi-job-designer"></use></svg>`,
+    kafka: `<i class="fa fa-fw fa-sitemap"></i>`,
+    mapreduce: `<i class="fa fa-fw fa-file-archive-o"></i>`,
+    markdown: `<svg class="hi hi-fw"><use xlink:href="#hi-markdown"></use></svg>`,
+    notebook: `<svg class="hi hi-fw"><use xlink:href="#hi-file-notebook"></use></svg>`,
+    oozie: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
+    'oozie-bundle': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-bundle"></use></svg>`,
+    'oozie-coordinator': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-coordinator"></use></svg>`,
+    'oozie-workflow': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-workflow"></use></svg>`,
+    pig: `<svg class="hi hi-fw"><use xlink:href="#hi-pig"></use></svg>`,
+    py: `<svg class="hi hi-fw"><use xlink:href="#hi-py"></use></svg>`,
+    pyspark: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
+    queries: `<i class="fa fa-fw fa-tasks"></i>`,
+    r: `<svg class="hi hi-fw"><use xlink:href="#hi-r"></use></svg>`,
+    report: `<i class="fa fa-fw fa-area-chart"></i>`,
+    s3: `<i class="fa fa-fw fa-cubes"></i>`,
+    scala: `<svg class="hi hi-fw"><use xlink:href="#hi-scala"></use></svg>`,
+    scheduler: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
+    security: `<i class="fa fa-fw fa-lock"></i>`,
+    shell: `<i class="fa fa-fw fa-terminal"></i>`,
+    solr: `<i class="fa fa-fw fa-search-plus"></i>`,
+    spark2: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
+    spark: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
+    sqoop1: `<svg class="hi hi-fw"><use xlink:href="#hi-sqoop"></use></svg>`,
+    sqoop: `<svg class="hi hi-fw"><use xlink:href="#hi-sqoop"></use></svg>`,
+    support: `<svg class="hi hi-fw"><use xlink:href="#hi-support"></use></svg>`,
+    tables: `<i class="fa fa-fw fa-database"></i>`,
+    text: `<i class="fa fa-fw fa-i-cursor"></i>`,
+    warehouses: `<i class="altus-icon altus-adb-cluster" style="margin: 0 1px 0 3px"></i>`,
+    workflows: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
+    yarn: `<i class="fa fa-fw fa-tasks"></i>`
+  };
+
+  const getIconHtml = (name: string): string => APP_ICON_INDEX[name] || APP_ICON_INDEX.default;
+
+  const USER_DRAWER_CHILDREN: Omit<SidebarNavigationItem, 'iconHtml'>[] = [];
+
+  if ((<hueWindow>window).USER_VIEW_EDIT_USER_ENABLED) {
+    USER_DRAWER_CHILDREN.push({
+      type: 'navigation',
+      name: 'userEdit',
+      displayName: I18n('My Profile'),
+      handler: (event: Event) =>
+        onHueLinkClick(event, '/useradmin/users/edit/' + (<hueWindow>window).LOGGED_USERNAME)
+    });
+  }
+
+  if ((<hueWindow>window).USER_IS_ADMIN || (<hueWindow>window).USER_IS_HUE_ADMIN) {
+    USER_DRAWER_CHILDREN.push({
+      type: 'navigation',
+      name: 'userAdmin',
+      displayName: I18n('Administer Users'),
+      handler: (event: Event) => onHueLinkClick(event, '/useradmin/users/')
+    });
+    USER_DRAWER_CHILDREN.push({
+      type: 'navigation',
+      name: 'serverAdmin',
+      displayName: I18n('Administer Server'),
+      handler: (event: Event) => onHueLinkClick(event, '/about/')
+    });
+  }
+
+  const HELP_DRAWER_CHILDREN: Omit<SidebarNavigationItem, 'iconHtml'>[] = [
+    {
+      type: 'navigation',
+      name: 'documentation',
+      displayName: I18n('Documentation'),
+      handler: (event: Event): void => onHueLinkClick(event, 'https://docs.gethue.com', '_blank')
+    },
+    {
+      type: 'navigation',
+      name: 'welcomeTour',
+      displayName: I18n('Welcome Tour'),
+      handler: (): void => {
+        huePubSub.publish('show.welcome.tour');
+      }
+    },
+    {
+      type: 'navigation',
+      name: 'gethue.com',
+      displayName: 'gethue.com',
+      handler: (event: Event): void => onHueLinkClick(event, 'https://gethue.com', '_blank')
+    }
+  ];
+
+  @Component({
+    components: { Sidebar }
+  })
+  export default class HueSidebar extends Vue {
+    sidebarItems: SidebarItem[] = [];
+    activeItemName: string | null = null;
+    isCollapsed = getFromLocalStorage('hue.sidebar.collapse', true);
+    drawerTopic: string | null = null;
+
+    userDrawerItem: UserDrawerItem = {
+      displayName: (<hueWindow>window).LOGGED_USERNAME || '',
+      logoutLabel: I18n('Log Out'),
+      logoutHandler: (event: Event) => onHueLinkClick(event, '/accounts/logout')
+    };
+    userDrawerChildren: SidebarAccordionSubItem[] = USER_DRAWER_CHILDREN;
+
+    helpDrawerItem: HelpDrawerItem = {
+      displayName: I18n('Help'),
+      iconHtml: getIconHtml('support')
+    };
+    helpDrawerChildren: SidebarAccordionSubItem[] = HELP_DRAWER_CHILDREN;
+
+    lastEditorDatabase?: EditorDatabaseDetails;
+    lastAssistDatabase?: AssistDatabaseDetails;
+
+    subTracker = new SubscriptionTracker();
+
+    toggleCollapsed(): void {
+      this.isCollapsed = !this.isCollapsed;
+      setInLocalStorage('hue.sidebar.collapse', this.isCollapsed);
+    }
+
+    mounted(): void {
+      const config = getLastKnownConfig();
+      if (config) {
+        this.hueConfigUpdated(config);
+      }
+      this.subTracker.subscribe(CONFIG_REFRESHED_EVENT, (refreshedConfig: HueConfig) => {
+        this.hueConfigUpdated(refreshedConfig);
+      });
+      this.subTracker.subscribe(
+        ASSIST_ACTIVE_DB_CHANGED_EVENT,
+        (details: AssistDatabaseDetails) => {
+          this.lastAssistDatabase = details;
+        }
+      );
+      this.subTracker.subscribe(ASSIST_SET_DATABASE_EVENT, (details: EditorDatabaseDetails) => {
+        this.lastEditorDatabase = details;
+      });
+
+      this.subTracker.subscribe('set.current.app.name', this.currentAppChanged.bind(this));
+      huePubSub.publish('get.current.app.name', this.currentAppChanged.bind(this));
+    }
+
+    @Provide()
+    selectedItemChanged(itemName: string): void {
+      if (
+        itemName !== 'user' &&
+        itemName !== 'help' &&
+        itemName !== 'sidebar-collapse-btn' &&
+        this.activeItemName !== itemName
+      ) {
+        this.activeItemName = itemName;
+      }
+    }
+
+    currentAppChanged(appName: string): void {
+      let adaptedName;
+
+      switch (appName) {
+        case 'editor':
+          const params = new URLSearchParams(location.search);
+          adaptedName = params.get('type') || appName;
+          break;
+        case 'filebrowser':
+          if (location.href.indexOf('=S3A') !== -1) {
+            adaptedName = 's3';
+          } else if (location.href.indexOf('=adl') !== -1) {
+            adaptedName = 'adls';
+          } else if (location.href.indexOf('=abfs') !== -1) {
+            adaptedName = 'abfs';
+          } else {
+            adaptedName = 'hdfs';
+          }
+          break;
+        case 'jobbrowser':
+          adaptedName = 'yarn';
+          break;
+        case 'home':
+          adaptedName = 'documents';
+          break;
+        case 'metastore':
+          adaptedName = 'tables';
+          break;
+        case 'oozie_bundle':
+          adaptedName = 'oozie-bundle';
+          break;
+        case 'oozie_coordinator':
+          adaptedName = 'oozie-coordinator';
+          break;
+        case 'oozie_workflow':
+          adaptedName = 'oozie-workflow';
+          break;
+        case 'security_hive':
+          adaptedName = 'security';
+          break;
+        case 'admin_wizard':
+        case 'userAdmin':
+        case 'userEdit':
+        case 'useradmin_edituser':
+        case 'useradmin_users':
+          adaptedName = 'user';
+          break;
+        case 'dashboard':
+        case 'hbase':
+        case 'importer':
+        case 'indexes':
+        case 'kafka':
+          break;
+        default:
+          console.warn('No sidebar alternative for app: ' + appName);
+      }
+
+      if (this.activeItemName !== adaptedName) {
+        this.identifyActive(adaptedName || appName);
+      }
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    identifyActive(possibleItemName: string): void {
+      if (!possibleItemName) {
+        return;
+      }
+
+      const findInside = (items: (SidebarItem | SidebarAccordionSubItem)[]): string | undefined => {
+        let found: string | undefined = undefined;
+        items.some(item => {
+          if ((<SidebarAccordionItem>item).children) {
+            found = findInside((<SidebarAccordionItem>item).children);
+          }
+          const navigationItem = <SidebarNavigationItem>item;
+          if (!found && navigationItem.name === possibleItemName) {
+            found = navigationItem.name;
+          }
+          return found;
+        });
+        return found;
+      };
+
+      const foundName = findInside(this.sidebarItems);
+      if (foundName) {
+        this.activeItemName = foundName;
+      }
+    }
+
+    hueConfigUpdated(clusterConfig: HueConfig): void {
+      const items: SidebarItem[] = [];
+
+      if (clusterConfig && clusterConfig.app_config) {
+        const favourite = clusterConfig.main_button_action;
+        const appsItems: SidebarItem[] = [];
+        const appConfig = clusterConfig.app_config;
+
+        [AppType.editor, AppType.dashboard, AppType.scheduler, AppType.sdkapps].forEach(appName => {
+          const config = appConfig[appName];
+
+          if ((<hueWindow>window).CUSTOM_DASHBOARD_URL && appName === 'dashboard') {
+            appsItems.push({
+              type: 'navigation',
+              name: 'dashboard',
+              displayName: I18n('Dashboard'),
+              iconHtml: getIconHtml('dashboard'),
+              handler: () => {
+                window.open((<hueWindow>window).CUSTOM_DASHBOARD_URL, '_blank');
+              }
+            });
+            return;
+          }
+          if (config && config.interpreters.length) {
+            if (config.interpreters.length === 1) {
+              appsItems.push({
+                type: 'navigation',
+                name: config.name,
+                displayName: config.displayName,
+                iconHtml: getIconHtml(config.name),
+                url: config.page,
+                handler: (event: Event) => onHueLinkClick(event, <string>config.page)
+              });
+            } else {
+              const subApps: SidebarAccordionSubItem[] = [];
+              let lastWasSql = false;
+              let dividerAdded = false;
+              config.interpreters.forEach(interpreter => {
+                if (!dividerAdded && lastWasSql && !(<EditorInterpreter>interpreter).is_sql) {
+                  subApps.push({ type: 'spacer' });
+                  dividerAdded = true;
+                }
+                const interpreterItem: SidebarAccordionSubItem = {
+                  type: 'navigation',
+                  name: interpreter.type,
+                  displayName: interpreter.displayName,
+                  url: interpreter.page,
+                  handler: (event: Event) => onHueLinkClick(event, interpreter.page)
+                };
+                if (favourite && favourite.page === interpreter.page) {
+                  // Put the favourite on top
+                  subApps.unshift(interpreterItem);
+                } else {
+                  subApps.push(interpreterItem);
+                }
+                lastWasSql = (<EditorInterpreter>interpreter).is_sql;
+              });
+
+              if (appName === 'editor' && (<hueWindow>window).SHOW_ADD_MORE_EDITORS) {
+                subApps.push({ type: 'spacer' });
+                const url = (<hueWindow>window).HAS_CONNECTORS
+                  ? '/desktop/connectors'
+                  : 'https://docs.gethue.com/administrator/configuration/connectors/';
+                subApps.push({
+                  type: 'navigation',
+                  name: 'addMoreInterpreters',
+                  displayName: I18n('Add more...'),
+                  url,
+                  handler: (event: Event) => onHueLinkClick(event, url)
+                });
+              }
+              const mainUrl = (<SidebarNavigationItem>subApps[0]).url || config.page || '/';
+              appsItems.push({
+                type: 'accordion',
+                name: config.name,
+                displayName: config.displayName,
+                url: mainUrl,
+                handler: (event: Event) => onHueLinkClick(event, mainUrl),
+                iconHtml: getIconHtml(config.name),
+                children: subApps
+              });
+            }
+          }
+        });
+
+        items.push(...appsItems);
+
+        const browserItems: SidebarItem[] = [];
+        if (appConfig.home) {
+          const url = appConfig.home.page || '/';
+          browserItems.push({
+            type: 'navigation',
+            name: 'documents',
+            displayName: appConfig.home.buttonName,
+            url,
+            handler: (event: Event) => onHueLinkClick(event, url),
+            iconHtml: getIconHtml('documents')
+          });
+        }
+        if (appConfig.browser && appConfig.browser.interpreters) {
+          appConfig.browser.interpreters.forEach(browser => {
+            if (browser.type === 'tables') {
+              browserItems.push({
+                type: 'navigation',
+                name: browser.type,
+                displayName: browser.displayName,
+                url: browser.page,
+                handler: (event: Event) => {
+                  const details = this.lastEditorDatabase || this.lastAssistDatabase;
+                  let url = browser.page;
+                  if (details) {
+                    url += `/${
+                      (<EditorDatabaseDetails>details).name ||
+                      (<AssistDatabaseDetails>details).database
+                    }?connector_id=${details.connector.id}`;
+                  }
+                  onHueLinkClick(event, url);
+                },
+                iconHtml: getIconHtml(browser.type)
+              });
+            } else {
+              browserItems.push({
+                type: 'navigation',
+                name: browser.type,
+                displayName: browser.displayName,
+                url: browser.page,
+                handler: (event: Event) => onHueLinkClick(event, browser.page),
+                iconHtml: getIconHtml(browser.type)
+              });
+            }
+          });
+        }
+        if (browserItems.length) {
+          if (items.length) {
+            items.push({ type: 'spacer' });
+          }
+
+          items.push(...browserItems);
+        }
+
+        this.sidebarItems = items;
+        if (!this.activeItemName) {
+          huePubSub.publish('get.current.app.name', this.currentAppChanged.bind(this));
+        }
+      }
+    }
+  }
+</script>

+ 5 - 13
desktop/core/src/desktop/js/ko/components/ko.sidebar.test.js → desktop/core/src/desktop/js/components/sidebar/HueSidebarWebComponent.ts

@@ -3,26 +3,18 @@
 // 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
+// '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,
+// 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 { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.sidebar';
+import HueSidebar from './HueSidebar.vue';
+import { wrap } from 'vue/webComponentWrapper';
 
-describe('ko.sidebar.js', () => {
-  const setup = koSetup();
-
-  it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {});
-
-    expect(element.innerHTML).toMatchSnapshot();
-  });
-});
+wrap('hue-sidebar-web-component', HueSidebar);

+ 100 - 0
desktop/core/src/desktop/js/components/sidebar/NavigationItem.vue

@@ -0,0 +1,100 @@
+<!--
+  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.
+-->
+
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div @mouseenter="showTooltip" @mouseleave="hideTooltip">
+    <BaseNavigationItem
+      :item="item"
+      :css-classes="isActive ? 'sidebar-sidebar-item sidebar-active' : 'sidebar-sidebar-item'"
+      @click="hideTooltip"
+    >
+      <div v-if="item.iconHtml" class="sidebar-sidebar-item-icon" v-html="item.iconHtml" />
+
+      <span>{{ item.displayName }}</span>
+      <div style="flex-grow: 1;" />
+
+      <BaseNavigationItemTooltip
+        v-if="tooltip"
+        :visible="isCollapsed"
+        :style="{ top: tooltip.top + 'px', left: tooltip.right + 'px' }"
+        role="tooltip"
+      >
+        <div
+          class="sidebar-sidebar-item-tooltip-primary"
+          :class="{ 'sidebar-active': isActive }"
+          :style="{ height: tooltip.height + 'px' }"
+        >
+          {{ item.displayName }}
+        </div>
+      </BaseNavigationItemTooltip>
+    </BaseNavigationItem>
+  </div>
+</template>
+<!-- eslint-enable vue/no-v-html -->
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import BaseNavigationItem from './BaseNavigationItem.vue';
+  import BaseNavigationItemTooltip from './BaseNavigationItemTooltip.vue';
+  import { SidebarNavigationItem } from './types';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+
+  @Component({
+    components: { BaseNavigationItemTooltip, BaseNavigationItem }
+  })
+  export default class NavigationItem extends Vue {
+    @Prop()
+    isCollapsed!: boolean;
+    @Prop()
+    item!: SidebarNavigationItem;
+    @Prop()
+    activeItemName!: string | null;
+
+    subTracker = new SubscriptionTracker();
+
+    get isActive(): boolean {
+      return this.item.name === this.activeItemName;
+    }
+
+    tooltip: DOMRect | null = null;
+
+    mounted(): void {
+      this.subTracker.addEventListener(<HTMLElement>this.$parent.$el, 'scroll', () => {
+        this.tooltip = null;
+      });
+    }
+
+    destroyed(): void {
+      this.subTracker.dispose();
+    }
+
+    showTooltip(event: MouseEvent): void {
+      if (!this.isCollapsed) {
+        return;
+      }
+      this.tooltip = (event.target as HTMLElement).getBoundingClientRect();
+    }
+
+    hideTooltip(): void {
+      this.tooltip = null;
+    }
+  }
+</script>

+ 38 - 0
desktop/core/src/desktop/js/components/sidebar/SectionItem.vue

@@ -0,0 +1,38 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="sidebar-sidebar-section-name">{{ item.displayName }}</div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop } from 'vue-property-decorator';
+  import { SidebarSectionItem } from './types';
+
+  @Component
+  export default class SectionItem extends Vue {
+    @Prop()
+    activeItemName?: string;
+    @Prop()
+    isCollapsed!: boolean;
+    @Prop()
+    item!: SidebarSectionItem;
+  }
+</script>

+ 177 - 0
desktop/core/src/desktop/js/components/sidebar/Sidebar.vue

@@ -0,0 +1,177 @@
+<!--
+  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.
+-->
+
+<template>
+  <Fragment>
+    <div class="sidebar-drawers" :class="{ 'sidebar-collapsed': isCollapsed }">
+      <SidebarDrawer
+        :show="drawerTopic === 'help'"
+        class="sidebar-user-drawer sidebar-right-drawer"
+        aria-label="Help"
+        @close="() => closeDrawer('help')"
+      >
+        <HelpDrawerContent :children="helpDrawerChildren" />
+      </SidebarDrawer>
+      <SidebarDrawer
+        :show="drawerTopic === 'user'"
+        class="sidebar-user-drawer sidebar-right-drawer"
+        aria-label="User Info"
+        @close="() => closeDrawer('user')"
+      >
+        <UserDrawerContent :item="userDrawerItem" :children="userDrawerChildren" />
+      </SidebarDrawer>
+    </div>
+    <div class="sidebar" :class="{ 'sidebar-collapsed': isCollapsed }">
+      <div class="sidebar-header">
+        <a href="javascript:void(0);">
+          <svg>
+            <use xlink:href="#hi-sidebar-logo" />
+          </svg>
+        </a>
+      </div>
+      <SidebarBody
+        :items="sidebarItems"
+        :is-collapsed="isCollapsed"
+        :active-item-name="activeItemName"
+      />
+      <div class="sidebar-footer">
+        <NavigationItem
+          v-if="helpItem"
+          :item="helpItem"
+          :is-collapsed="isCollapsed"
+          :active-item-name="activeItemName"
+        />
+        <NavigationItem
+          v-if="userItem"
+          :item="userItem"
+          :is-collapsed="isCollapsed"
+          :active-item-name="activeItemName"
+        />
+        <div class="sidebar-footer-bottom-row">
+          <div class="sidebar-footer-version-number"><!-- TODO: Add Version --></div>
+          <BaseNavigationItem
+            :css-classes="'sidebar-footer-collapse-btn'"
+            :item="{
+              handler: () => $emit('toggle-collapsed'),
+              name: 'sidebar-collapse-btn'
+            }"
+          >
+            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"
+              />
+            </svg>
+          </BaseNavigationItem>
+        </div>
+        <div class="sidebar-footer-bottom-color-line" />
+      </div>
+    </div>
+  </Fragment>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Fragment } from 'vue-fragment';
+  import { Prop } from 'vue-property-decorator';
+  import BaseNavigationItem from './BaseNavigationItem.vue';
+  import './drawer.scss';
+  import HelpDrawerContent from './HelpDrawerContent.vue';
+  import NavigationItem from './NavigationItem.vue';
+  import './sidebar.scss';
+  import SidebarBody from './SidebarBody.vue';
+  import SidebarDrawer from './SidebarDrawer.vue';
+  import {
+    SidebarAccordionSubItem,
+    SidebarItem,
+    SidebarNavigationItem,
+    UserDrawerItem
+  } from './types';
+  import UserDrawerContent from './UserDrawerContent.vue';
+
+  @Component({
+    components: {
+      HelpDrawerContent,
+      UserDrawerContent,
+      SidebarDrawer,
+      Fragment,
+      NavigationItem,
+      BaseNavigationItem,
+      SidebarBody
+    }
+  })
+  export default class Sidebar extends Vue {
+    @Prop()
+    sidebarItems!: SidebarItem[];
+
+    @Prop({ required: false })
+    userDrawerItem: UserDrawerItem | null = null;
+    @Prop({ required: false })
+    userDrawerChildren: SidebarAccordionSubItem[] = [];
+
+    @Prop({ required: false })
+    helpDrawerItem: HelpDrawerItem | null = null;
+    @Prop({ required: false })
+    helpDrawerChildren: SidebarAccordionSubItem[] = [];
+
+    @Prop({ required: false })
+    activeItemName?: string;
+    @Prop({ required: false, default: true })
+    isCollapsed?: boolean;
+
+    drawerTopic: string | null = null;
+
+    closeDrawer(topic: string): void {
+      if (this.drawerTopic === topic) {
+        this.drawerTopic = null;
+      }
+    }
+
+    get helpItem(): SidebarNavigationItem | null {
+      if (!this.helpDrawerItem) {
+        return null;
+      }
+      return {
+        type: 'navigation',
+        name: 'help',
+        displayName: this.helpDrawerItem.displayName,
+        iconHtml: this.helpDrawerItem.iconHtml,
+        handler: () => {
+          this.drawerTopic = 'help';
+        }
+      };
+    }
+
+    get userItem(): SidebarNavigationItem | null {
+      if (!this.userDrawerItem) {
+        return null;
+      }
+      return {
+        type: 'navigation',
+        name: 'user',
+        displayName: this.userDrawerItem.displayName,
+        iconHtml: `<div class="sidebar-user-icon" role="img">${this.userDrawerItem.displayName[0].toUpperCase()}</div>`,
+        handler: () => {
+          this.drawerTopic = 'user';
+        }
+      };
+    }
+  }
+</script>

+ 119 - 0
desktop/core/src/desktop/js/components/sidebar/SidebarBody.vue

@@ -0,0 +1,119 @@
+<!--
+  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.
+-->
+
+<template>
+  <nav
+    ref="sidebarNav"
+    v-overflow-on-hover="{ direction: 'y' }"
+    class="sidebar-body"
+    :class="{
+      'sidebar-body-overflow-top': showOverflowIndicatorTop,
+      'sidebar-body-overflow-bottom': showOverflowIndicatorBtm
+    }"
+    @scroll="onScroll"
+  >
+    <template v-for="(item, index) in items">
+      <NavigationItem
+        v-if="item.type === 'navigation'"
+        :key="item.name"
+        :item="item"
+        :active-item-name="activeItemName"
+        :is-collapsed="isCollapsed"
+      />
+      <AccordionItem
+        v-else-if="item.type === 'accordion'"
+        :key="item.name"
+        :item="item"
+        :active-item-name="activeItemName"
+        :is-collapsed="isCollapsed"
+      />
+      <SectionItem
+        v-else-if="item.type === 'section'"
+        :key="item.name"
+        :item="item"
+        :active-item-name="activeItemName"
+        :is-collapsed="isCollapsed"
+      />
+      <SpacerItem v-else :key="'space-' + index" />
+    </template>
+  </nav>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop, Watch } from 'vue-property-decorator';
+  import { overflowOnHover } from 'components/directives/overflowOnHoverDirective';
+  import AccordionItem from './AccordionItem.vue';
+  import NavigationItem from './NavigationItem.vue';
+  import SectionItem from './SectionItem.vue';
+  import SpacerItem from './SpacerItem.vue';
+  import { SidebarItem } from './types';
+  import { defer } from 'utils/hueUtils';
+
+  @Component({
+    components: { SpacerItem, NavigationItem, SectionItem, AccordionItem },
+    directives: {
+      'overflow-on-hover': overflowOnHover
+    }
+  })
+  export default class SidebarBody extends Vue {
+    @Prop()
+    items!: SidebarItem[];
+    @Prop()
+    isCollapsed!: boolean;
+    @Prop({ required: false })
+    activeItemName: string | null = null;
+
+    showOverflowIndicatorTop = false;
+    showOverflowIndicatorBtm = false;
+
+    @Watch('items', { immediate: true })
+    onItemsChange(): void {
+      defer(() => {
+        this.detectOverflow(<HTMLElement>this.$refs.sidebarNav);
+      });
+    }
+
+    onScroll(evt: Event): void {
+      const el = <HTMLElement | null>evt.target;
+      this.detectOverflow(el);
+    }
+
+    detectOverflow(el?: HTMLElement | null): void {
+      if (!el) {
+        return;
+      }
+      const hasOverflowOnTop = el.scrollTop > 0;
+      const hasOverflowOnBtm = el.scrollHeight - el.scrollTop > el.clientHeight;
+      if (hasOverflowOnTop && hasOverflowOnBtm) {
+        this.showOverflowIndicatorTop = true;
+        this.showOverflowIndicatorBtm = true;
+      } else if (hasOverflowOnTop) {
+        this.showOverflowIndicatorTop = true;
+        this.showOverflowIndicatorBtm = false;
+      } else if (hasOverflowOnBtm) {
+        this.showOverflowIndicatorTop = false;
+        this.showOverflowIndicatorBtm = true;
+      } else {
+        this.showOverflowIndicatorTop = false;
+        this.showOverflowIndicatorBtm = false;
+      }
+    }
+  }
+</script>

+ 65 - 0
desktop/core/src/desktop/js/components/sidebar/SidebarDrawer.vue

@@ -0,0 +1,65 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="sidebar-drawer" role="dialog" :class="{ open: show }" :aria-hidden="!show">
+    <slot v-if="show" />
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Prop, Provide, Watch } from 'vue-property-decorator';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import { defer } from 'utils/hueUtils';
+
+  @Component
+  export default class SidebarDrawer extends Vue {
+    @Prop()
+    show!: boolean;
+
+    subTracker = new SubscriptionTracker();
+    deferredShow = false;
+
+    mounted(): void {
+      this.subTracker.addEventListener(document, 'click', (event: MouseEvent) => {
+        if (
+          this.deferredShow &&
+          document.contains(<Node>event.target) &&
+          !this.$el.contains(<Node>event.target)
+        ) {
+          this.$emit('close');
+        }
+      });
+    }
+
+    @Watch('show', { immediate: true })
+    showChanged(newValue: boolean): void {
+      // deferredShow is used to prevent closing it immediately after the document click event triggered by opening
+      defer(() => {
+        this.deferredShow = newValue;
+      });
+    }
+
+    @Provide()
+    hideDrawer(): void {
+      this.$emit('close');
+    }
+  }
+</script>

+ 29 - 0
desktop/core/src/desktop/js/components/sidebar/SpacerItem.vue

@@ -0,0 +1,29 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="sidebar-sidebar-spacer" />
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+
+  @Component
+  export default class SpacerItem extends Vue {}
+</script>

+ 69 - 0
desktop/core/src/desktop/js/components/sidebar/UserDrawerContent.vue

@@ -0,0 +1,69 @@
+<!--
+  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.
+-->
+
+<template>
+  <div class="sidebar-drawer-user-content">
+    <div class="sidebar-drawer-user-content-header">
+      <div class="sidebar-user-icon" role="img">{{ item.displayName[0].toUpperCase() }}</div>
+      <div>
+        <div>{{ item.displayName }}</div>
+        <small v-if="item.userEmail">{{ item.userEmail }}</small>
+      </div>
+    </div>
+    <ul>
+      <li v-for="childItem in children" :key="childItem.name">
+        <BaseNavigationItem :item="childItem" @click="hideDrawer">
+          {{ childItem.displayName }}
+        </BaseNavigationItem>
+      </li>
+      <li>
+        <BaseNavigationItem :item="logoutItem" @click="hideDrawer">
+          {{ item.logoutLabel }}
+        </BaseNavigationItem>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import Component from 'vue-class-component';
+  import { Inject, Prop } from 'vue-property-decorator';
+  import BaseNavigationItem from './BaseNavigationItem.vue';
+  import { SidebarAccordionSubItem, SidebarNavigationItem, UserDrawerItem } from './types';
+
+  @Component({
+    components: { BaseNavigationItem }
+  })
+  export default class UserDrawerContent extends Vue {
+    @Inject()
+    hideDrawer!: () => void;
+
+    @Prop()
+    item!: UserDrawerItem;
+    @Prop({ required: false, default: [] })
+    children?: SidebarAccordionSubItem[];
+
+    get logoutItem(): Pick<SidebarNavigationItem, 'handler' | 'name'> {
+      return {
+        handler: this.item.logoutHandler,
+        name: 'logout'
+      };
+    }
+  }
+</script>

+ 432 - 0
desktop/core/src/desktop/js/components/sidebar/drawer.scss

@@ -0,0 +1,432 @@
+// 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 '../styles/colors';
+@import '../styles/variables';
+@import './variables';
+
+$default-drawer-width: 248px;
+$large-drawer-width: 400px;
+
+$sidebar-drawer-in-front-z-index: $sidebar-z-index + 1;
+$sidebar-drawer-behind-z-index: $sidebar-z-index - 1;
+
+.sidebar-drawer {
+  position: fixed;
+  z-index: $sidebar-drawer-behind-z-index;
+  top: 0;
+  bottom: 0;
+  left: -$default-drawer-width;
+  width: $default-drawer-width;
+  overflow: hidden;
+  transition: all $animation-duration-base;
+  background: #fff;
+  opacity: 0;
+}
+
+.sidebar-drawer.open {
+  left: 0;
+  box-shadow: 0 0 16px rgba(0, 0, 0, 0.28);
+  opacity: 1;
+}
+
+.sidebar-drawer.sidebar-app-drawer {
+  white-space: nowrap;
+  z-index: $sidebar-drawer-in-front-z-index;
+  display: flex;
+  height: 100%;
+  flex-direction: column;
+
+  .cdp-app-switcher-loading {
+    padding: 24px;
+  }
+
+  .cdp-app-switcher-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 24px;
+    margin: 0 0 $fluid-spacing-xs 0;
+    height: 56px;
+    flex-shrink: 0;
+  }
+
+  .cdp-app-switcher-indicator {
+    width: $sidebar-indicator-width;
+    height: 100%;
+  }
+
+  .cdp-app-switcher-app-icon {
+    margin-left: $fluid-spacing-m - $sidebar-indicator-width;
+  }
+
+  .cdp-app-switcher-app-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    overflow: auto;
+
+    a {
+      height: 48px;
+      display: flex;
+      align-items: center;
+      transition: background-color $animation-duration-base;
+
+      &:hover {
+        background-color: $fluid-gray-100;
+      }
+
+      svg {
+        font-size: 32px;
+        display: block;
+      }
+
+      span {
+        display: inline-block;
+        margin-left: 16px;
+        font-size: 14px;
+        color: $fluid-gray-700;
+      }
+    }
+  }
+}
+
+.sidebar-drawer-common-menu {
+  display: flex;
+  height: 100%;
+  flex-direction: column;
+}
+
+.sidebar-drawer-common-menu-header {
+  height: $sidebar-header-height;
+  border-bottom: 1px solid $fluid-gray-100;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-shrink: 0;
+  padding: 0 24px;
+
+  img {
+    height: 20px;
+    width: 20px;
+  }
+}
+
+.sidebar-drawer-common-menu-body {
+  padding: 24px;
+  border-bottom: 1px solid $fluid-gray-100;
+  flex-grow: 1;
+  min-height: 0;
+  overflow: auto;
+}
+
+.sidebar-drawer.sidebar-help-drawer {
+  left: -$large-drawer-width;
+  width: $large-drawer-width;
+
+  .menu-footer {
+    flex-shrink: 0;
+
+    h2 {
+      padding: 24px 24px 0 24px;
+    }
+  }
+
+  ul.nav-list {
+    list-style: none;
+    margin: 4px 0;
+    padding: 0;
+
+    li a {
+      height: 32px;
+      display: flex;
+      align-items: center;
+      color: $fluid-gray-700;
+      padding: 0 24px;
+
+      &:hover {
+        background-color: $fluid-gray-200;
+      }
+
+      svg {
+        margin-right: $fluid-spacing-xs;
+        font-size: 20px;
+        height: 20px;
+        width: 20px;
+      }
+    }
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  p {
+    padding: 0;
+    color: $fluid-gray-800;
+    margin: 0 0 0.5em 0;
+  }
+
+  h1 {
+    font-size: 28px;
+    line-height: 36px;
+    font-weight: 300;
+    margin: 0;
+  }
+
+  h2 {
+    font-size: 24px;
+    line-height: 32px;
+    font-weight: 300;
+  }
+
+  h3 {
+    font-size: 20px;
+    line-height: 28px;
+    font-weight: 300;
+  }
+
+  h4 {
+    font-size: 16px;
+    line-height: 20px;
+    font-weight: 500;
+  }
+
+  h5 {
+    font-size: 14px;
+    line-height: 20px;
+    font-weight: 500;
+  }
+
+  h6 {
+    font-size: 12px;
+    line-height: 20px;
+    font-weight: 500;
+  }
+
+  p {
+    margin-bottom: 1em;
+    line-height: 20px;
+  }
+
+  .menu-body {
+    ul {
+      list-style-type: disc;
+
+      ul {
+        list-style-type: circle;
+
+        ul {
+          list-style-type: square;
+        }
+      }
+    }
+
+    ol {
+      list-style-type: decimal;
+    }
+
+    ol,
+    ul {
+      display: block;
+      line-height: 20px;
+      margin-block-start: 1em;
+      margin-block-end: 1em;
+      padding-inline-start: 40px;
+
+      ol,
+      ul {
+        margin-block-start: 0;
+        margin-block-end: 0;
+      }
+
+      li {
+        display: list-item;
+      }
+    }
+  }
+}
+
+.sidebar-drawer.sidebar-version-drawer {
+  left: -$large-drawer-width;
+  width: $large-drawer-width;
+
+  footer {
+    flex-shrink: 0;
+    padding: 24px;
+  }
+
+  dt {
+    font-size: $font-size-lg;
+  }
+
+  dd {
+    margin-bottom: $fluid-spacing-s;
+  }
+
+  ul {
+    list-style: none;
+    padding-inline-start: 0;
+  }
+}
+
+.sidebar-version-drawer-related-docs {
+  flex-shrink: 0;
+}
+
+.sidebar-drawer.sidebar-help-drawer,
+.sidebar-drawer.sidebar-user-drawer {
+  top: auto;
+  height: auto;
+  bottom: 20px;
+  transition: none;
+
+  .menu-header {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding: 10px 16px 20px 16px;
+
+    .user-icon {
+      width: 36px;
+      height: 36px;
+      margin-right: 12px;
+      font-size: 22px;
+      flex-shrink: 0;
+    }
+
+    > div {
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
+    }
+
+    small {
+      font-size: $font-size-sm;
+    }
+  }
+}
+
+.sidebar-drawer-help-content,
+.sidebar-drawer-user-content {
+  ul {
+    list-style: none;
+    margin: 4px 0;
+    padding: 0;
+
+    li a,
+    li button {
+      height: 32px;
+      display: flex;
+      align-items: center;
+      color: $fluid-gray-700;
+      padding: 0 24px;
+      width: 100%;
+
+      &:hover {
+        background-color: $fluid-gray-200;
+      }
+
+      svg {
+        margin-left: 8px;
+        font-size: 16px;
+      }
+    }
+  }
+}
+
+.sidebar-drawer-help-content-header {
+  h2 {
+    padding: 24px 24px 0 24px;
+    font-size: 24px;
+    line-height: 32px;
+    font-weight: 300;
+  }
+}
+
+.sidebar-drawer-user-content-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 10px 16px 20px 16px;
+
+  .sidebar-user-icon {
+    width: 36px;
+    height: 36px;
+    margin-right: 12px;
+    font-size: 22px;
+    flex-shrink: 0;
+  }
+
+  > div {
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+
+  small {
+    font-size: $font-size-sm;
+  }
+}
+
+.sidebar-drawer.sidebar-tour-drawer {
+  left: -$large-drawer-width;
+  width: $large-drawer-width;
+
+  .cdp-panel-header {
+    height: $sidebar-header-height;
+    border-bottom: 1px solid $fluid-gray-300;
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    padding: 0 24px;
+
+    h1 {
+      flex: 1;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      font-size: 20px;
+      font-weight: 400;
+      color: $fluid-gray-900;
+    }
+
+    img {
+      height: 20px;
+      width: 20px;
+    }
+
+    .cdp-back {
+      margin-right: 8px;
+    }
+  }
+
+  .cdp-panel-body {
+    padding: 20px 24px;
+
+    h2 {
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      font-size: 16px;
+      font-weight: 400;
+      color: $fluid-gray-900;
+      margin-bottom: 20px;
+    }
+  }
+}

+ 53 - 0
desktop/core/src/desktop/js/components/sidebar/hueSidebar.scss

@@ -0,0 +1,53 @@
+// 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 '../styles/colors';
+@import '../styles/variables';
+
+.sidebar {
+
+  // This overrides global scrollbar styles, temporary until the global ones are removed.
+  ::-webkit-scrollbar-thumb {
+    border: none;
+    background-color: $fluid-gray-900;
+  }
+
+  ::-webkit-scrollbar-thumb:hover {
+    background-color: $fluid-gray-700;
+  }
+
+  &.sidebar-collapsed {
+    .sidebar-header a {
+      overflow: hidden;
+      width: 36px;
+      display: block;
+
+      > svg {
+        color: $fluid-white;
+        height: 30px;
+        width: 110px;
+      }
+    }
+  }
+
+  &:not(.sidebar-collapsed) {
+    .sidebar-header a > svg {
+      color: $hue-primary-color-dark;
+      height: 25px;
+      margin-right: 24px;
+    }
+  }
+}

+ 470 - 0
desktop/core/src/desktop/js/components/sidebar/sidebar.scss

@@ -0,0 +1,470 @@
+// 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 '../styles/colors';
+@import '../styles/variables';
+@import './variables';
+
+@mixin sidebar-active-side-indicator {
+  background-color: $sidebar-selection-color;
+  content: ' ';
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: $sidebar-indicator-width;
+}
+
+.sidebar,
+.sidebar-drawers {
+  // Not all products set the same defaults as we do in thunderhead.
+  // We'll set the most basic ones here.
+  font-size: $font-size-base;
+  color: $text-color;
+  line-height: $line-height-base;
+  -webkit-font-smoothing: antialiased;
+}
+
+.sidebar {
+  width: $sidebar-width;
+  height: 100%;
+  transition: all $sidebar-animation-duration-base;
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+  background-color: $sidebar-bg-color;
+  z-index: $sidebar-z-index;
+  position: relative;
+  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='248' height='196' viewBox='0 0 248 196'%3E%3Cdefs%3E%3Cpath id='6zd0wr4qxa' d='M0 0H248V196H0z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg%3E%3Cg%3E%3Cg transform='translate(-125 -842) translate(125 270) translate(0 572)'%3E%3Cmask id='4w2gcddqub' fill='%23fff'%3E%3Cuse xlink:href='%236zd0wr4qxa'/%3E%3C/mask%3E%3Cuse fill='%23132329' xlink:href='%236zd0wr4qxa'/%3E%3Cg mask='url(%234w2gcddqub)' opacity='.503'%3E%3Cg%3E%3Cpath fill='%2319323C' d='M117.744 164.685L0 282.43 42.867 282.43 160.612 164.685z' transform='translate(53 -87)'/%3E%3Cpath fill='%23224452' d='M282.73 0L117.913 164.685 160.612 164.685 42.868 282.429 349.591 282.459 349.591 0.002z' transform='translate(53 -87)'/%3E%3Cg%3E%3Cpath fill='%23224452' d='M148.278 0L146.067 0 28.322 117.745 30.533 117.745zM141.346 0L23.602 117.745 25.813 117.745 143.558 0zM35.253 117.745L152.998 0 150.786 0 33.041 117.745zM39.973 117.745L157.718 0 155.507 0 37.762 117.745zM136.626 0L18.881 117.745 21.092 117.745 138.838 0zM122.466 0L4.721 117.745 6.933 117.745 124.677 0zM117.746 0L0 117.745 2.212 117.745 119.956 0zM127.187 0L9.441 117.745 11.653 117.745 129.398 0zM131.907 0L14.161 117.745 16.372 117.745 134.118 0z' transform='translate(53 -87) translate(13.957 108.673)'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");
+  background-position: bottom;
+  background-repeat: no-repeat;
+
+  a {
+    color: $sidebar-default-text-color;
+  }
+}
+
+.sidebar-drawers {
+  a {
+    cursor: pointer;
+    text-decoration: none;
+
+    &[target='_blank']:not(.no-external-icon)::after {
+      content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"><path fill="%235a656d" fill-rule="evenodd" d="M13 3v2h4.586l-6.293 6.293 1.415 1.414L19 6.414V11h2V3h-8zM3 3v18h18v-6h-2v4H5V5h4V3H3z"/></svg>');
+      padding-left: 4px;
+      vertical-align: middle;
+    }
+  }
+
+  .sidebar-right-drawer.open {
+    left: $sidebar-width;
+    transition: all $animation-duration-slow ease-out;
+  }
+}
+
+.sidebar-base-btn {
+  background-color: transparent;
+  border: none;
+  cursor: pointer;
+  display: inline;
+  margin: 0;
+  padding: 0;
+
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    outline: none;
+  }
+}
+
+.sidebar-base-link {
+  cursor: pointer;
+
+  &:hover,
+  &:focus,
+    // CDP less has some very specific style we'll need to override
+  &:focus:not(:hover):not(.ant-btn) {
+    text-decoration: none;
+    outline: none;
+  }
+}
+
+.sidebar-header {
+  height: $sidebar-header-height;
+  display: flex;
+  flex-shrink: 0;
+  align-items: center;
+  color: $fluid-white;
+  padding: 0 0 0 $fluid-spacing-m;
+  font-size: 16px;
+
+  & > a {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.sidebar-app-switcher-trigger {
+  transition: font-size $sidebar-animation-duration-base;
+  margin-right: $fluid-spacing-xxs;
+  flex-shrink: 0;
+  font-size: 20px;
+  color: $sidebar-default-text-color;
+  line-height: 1;
+
+  svg {
+    vertical-align: -3px;
+  }
+}
+
+.sidebar-header-product-icon-logo {
+  width: 40px;
+  height: 40px;
+}
+
+.sidebar-header-product-text-logo {
+  margin-left: $fluid-spacing-xxs;
+  white-space: nowrap;
+  overflow: hidden;
+  height: 40px;
+}
+
+.sidebar-body {
+  flex: auto;
+  overflow: hidden auto;
+  padding-top: $fluid-spacing-m;
+  position: relative;
+
+  > div {
+    // Prevent jumping contents when scrollbar is visible
+    min-width: 56px;
+  }
+
+  // avoid moving context by 1px when the overflow indicators are shown
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+
+  &.sidebar-body-overflow-top {
+    border-top: 1px solid $sidebar-overflow-indicator-color;
+  }
+
+  &.sidebar-body-overflow-bottom {
+    border-bottom: 1px solid $sidebar-overflow-indicator-color;
+  }
+}
+
+.sidebar-sidebar-section-name,
+.sidebar-sidebar-item,
+.sidebar-sidebar-spacer {
+  height: $sidebar-item-height;
+  display: flex;
+  align-items: center;
+  color: $sidebar-default-text-color;
+  padding-left: $fluid-spacing-m;
+}
+
+.sidebar-sidebar-section-name {
+  opacity: 1;
+  margin-top: $fluid-spacing-s;
+  white-space: nowrap;
+}
+
+.sidebar-sidebar-item {
+  position: relative;
+
+  &.sidebar-active::before {
+    @include sidebar-active-side-indicator;
+  }
+
+  &.sidebar-active {
+    color: $sidebar-selection-color;
+    font-weight: 500;
+  }
+
+  &:hover:not(.sidebar-active) {
+    color: $sidebar-hover-color;
+  }
+
+  &:focus:not(:hover):not(.sidebar-active),
+  &:hover:focus:not(.sidebar-active) {
+    color: $sidebar-hover-color;
+    outline: none;
+  }
+
+  span {
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+}
+
+.sidebar-sidebar-item-icon {
+  height: 20px;
+  width: 20px;
+  display: flex;
+  justify-content: center;
+  margin-right: $fluid-spacing-xs;
+  transition-duration: $sidebar-animation-duration-base;
+  // using the `all` syntax seemed to cause the icon color change to be delayed
+  // compared to the rest of the icon.
+  transition-property: height, width, flex, margin;
+
+  i,
+  svg {
+    transition: font-size $sidebar-animation-duration-base;
+    font-size: $font-size-xl !important;
+  }
+}
+
+.sidebar-sidebar-item-tooltip {
+  position: fixed;
+  background-color: $sidebar-bg-color;
+  overflow: hidden;
+  width: 233px;
+  z-index: 2;
+  opacity: 0;
+
+  // when displaying accordion items in a tooltip, they could overlaps the bottom of the page
+  // in which case we introduce a scrollbar on the items, which works best when this is flex.
+  display: flex;
+  flex-direction: column;
+  color: $sidebar-hover-color;
+  transition: opacity $sidebar-animation-duration-base;
+
+  &.sidebar-sidebar-item-tooltip-visible {
+    opacity: 1;
+  }
+
+  .sidebar-sidebar-item-accordion-sub-item {
+    margin-left: $fluid-spacing-s;
+  }
+}
+
+.sidebar-sidebar-item-tooltip-primary {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+
+  &.sidebar-active {
+    color: $sidebar-selection-color;
+    font-weight: 500;
+  }
+}
+
+.sidebar-accordion-item {
+  position: relative;
+
+  &.sidebar-active::before {
+    @include sidebar-active-side-indicator;
+  }
+}
+
+.sidebar-accordion-item-btn-open {
+  color: $sidebar-hover-color;
+}
+
+.sidebar-accordion-collapse-icon {
+  position: absolute;
+  top: 13px;
+  left: 6px;
+  transition: transform $sidebar-animation-duration-base;
+
+  svg {
+    display: block;
+  }
+}
+
+.sidebar-accordion-collapse-icon-open {
+  transform: rotate(90deg);
+}
+
+.sidebar-accordion-items {
+  height: 0;
+  transition: height $sidebar-animation-duration-base;
+  overflow: hidden;
+}
+
+.sidebar-sidebar-item-tooltip-accordion-items {
+  overflow-x: hidden;
+  border-top: 1px solid transparent;
+}
+
+.sidebar-sidebar-item-tooltip-accordion-items-overflow-top {
+  border-top: 1px solid $sidebar-overflow-indicator-color;
+}
+
+.sidebar-sidebar-item-tooltip-extra-hover-area {
+  position: fixed;
+  cursor: pointer;
+  width: 30px;
+  height: 80px;
+  transform: rotate(339deg);
+  transform-origin: 100% 0;
+  z-index: 1;
+}
+
+.sidebar-sidebar-item-accordion-sub-item {
+  font-size: $font-size-sm;
+  margin-left: 52px;
+  display: flex;
+  align-items: center;
+  height: 32px;
+  color: $sidebar-default-text-color;
+
+  &:hover,
+  &:focus {
+    color: $sidebar-hover-color;
+  }
+}
+
+.sidebar-sidebar-item-accordion-sub-item-active {
+  color: $sidebar-selection-color;
+  font-weight: 500;
+}
+
+.sidebar-footer-bottom-row {
+  display: flex;
+  align-items: center;
+  margin-top: 12px;
+}
+
+.sidebar-footer-version-number {
+  margin-left: $fluid-spacing-m;
+  color: $sidebar-default-text-color;
+  flex-grow: 1;
+  font-size: $font-size-sm;
+  overflow: hidden;
+}
+
+.sidebar-footer-collapse-btn {
+  display: flex;
+  flex-shrink: 0;
+  flex-grow: 1;
+  align-items: center;
+  justify-content: flex-end;
+  height: 52px;
+  padding: 0 16px;
+
+  svg,
+  img {
+    height: 24px;
+    width: 24px;
+    transition: all $sidebar-animation-duration-base;
+    transition-delay: $sidebar-animation-duration-base;
+  }
+}
+
+.sidebar-footer-bottom-color-line {
+  height: 4px;
+  background-color: $sidebar-selection-color;
+}
+
+.sidebar-user-icon {
+  background-color: $sidebar-selection-color;
+  border-radius: 50%;
+  width: 20px;
+  height: 20px;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: $sidebar-bg-color;
+  text-transform: uppercase;
+}
+
+.sidebar-collapsed {
+  width: $sidebar-collapsed-width;
+
+  .sidebar-header {
+    padding: 0 0 0 11px;
+    position: relative;
+    background: linear-gradient(to bottom right, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.1) 50%), $sidebar-selection-color;
+  }
+
+  .sidebar-header-logo {
+    display: none;
+  }
+
+  .sidebar-app-switcher-trigger {
+    font-size: 32px;
+    color: $sidebar-bg-color;
+
+    svg {
+      vertical-align: -4px;
+    }
+  }
+
+  .sidebar-sidebar-section-name {
+    width: 0;
+    overflow: hidden;
+  }
+
+  .sidebar-sidebar-item {
+    padding-left: 0;
+    justify-content: center;
+
+    // make <button> and <a> behave the same
+    width: 100%;
+
+    span {
+      opacity: 0;
+      display: none;
+    }
+  }
+
+  .sidebar-sidebar-item-icon {
+    height: 24px;
+    width: 24px;
+    flex: 0 0 $sidebar-collapsed-width;
+    margin: 0;
+
+    svg {
+      font-size: 24px !important;
+    }
+  }
+
+  .sidebar-accordion-collapse-icon {
+    display: none;
+  }
+
+  .sidebar-accordion-items {
+    display: none;
+  }
+
+  .sidebar-footer-version-number {
+    display: none;
+  }
+
+  .sidebar-footer-collapse-btn {
+    svg,
+    img {
+      height: 24px;
+      width: 24px;
+      transform: rotate(180deg);
+    }
+  }
+
+  .sidebar-user-icon {
+    width: 24px;
+    height: 24px;
+    font-size: 16px;
+  }
+
+  .sidebar-right-drawer.open {
+    left: $sidebar-collapsed-width;
+  }
+}

+ 65 - 0
desktop/core/src/desktop/js/components/sidebar/types.ts

@@ -0,0 +1,65 @@
+// 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.
+
+export interface SidebarNavigationItem {
+  type: 'navigation';
+  name: string;
+  displayName: string;
+  url?: string;
+  handler?: (event: Event) => void;
+  iconHtml: string;
+}
+
+export type SidebarAccordionSubItem = SidebarSpacerItem | Omit<SidebarNavigationItem, 'iconHtml'>;
+
+export interface SidebarSpacerItem {
+  type: 'spacer';
+}
+
+export interface SidebarAccordionItem {
+  type: 'accordion';
+  name: string;
+  iconHtml: string;
+  displayName: string;
+  url?: string;
+  handler?: (event: Event) => void;
+  children: SidebarAccordionSubItem[];
+}
+
+export interface SidebarSectionItem {
+  type: 'section';
+  name: string;
+  displayName: string;
+  children: (SidebarNavigationItem | SidebarSpacerItem | SidebarAccordionItem)[];
+}
+
+export type SidebarItem =
+  | SidebarNavigationItem
+  | SidebarSpacerItem
+  | SidebarAccordionItem
+  | SidebarSectionItem;
+
+export interface UserDrawerItem {
+  logoutHandler: (event: Event) => void;
+  logoutLabel: string;
+  displayName: string;
+  userEmail?: string;
+}
+
+export interface HelpDrawerItem {
+  displayName: string;
+  iconHtml: string;
+}

+ 16 - 13
desktop/core/src/desktop/js/ko/components/ko.multiClusterSidebar.test.js → desktop/core/src/desktop/js/components/sidebar/variables.scss

@@ -3,26 +3,29 @@
 // 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
+// '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,
+// 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 { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.multiClusterSidebar';
+@import '../styles/colors';
+@import '../styles/variables';
 
-describe('ko.multiClusterSidebar.js', () => {
-  const setup = koSetup();
-
-  it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {});
-
-    expect(element.innerHTML).toMatchSnapshot();
-  });
-});
+$sidebar-selection-color: $hue-primary-color-dark !default;
+$sidebar-width: 248px;
+$sidebar-collapsed-width: 56px;
+$sidebar-bg-color: #132329;
+$sidebar-overflow-indicator-color: #517989;
+$sidebar-default-text-color: $fluid-gray-400;
+$sidebar-hover-color: $fluid-white;
+$sidebar-item-height: 40px;
+$sidebar-animation-duration-base: $animation-duration-base;
+$sidebar-header-height: 56px;
+$sidebar-indicator-width: 4px;
+$sidebar-z-index: 10;

+ 21 - 0
desktop/core/src/desktop/js/components/styles/variables.scss

@@ -16,4 +16,25 @@
  limitations under the License.
 */
 
+@import './colors';
+
+$animation-duration-base: 0.2s;
+$animation-duration-slow: 0.3s;
+$text-color: $fluid-gray-900;
+$line-height-base: 1.4285;
+
+$font-size-sm: 12px;
+$font-size-base: 14px;
+$font-size-lg: 16px;
+$font-size-xl: 20px;
+
+$fluid-spacing-xxs: 4px;
+$fluid-spacing-xs: 8px;
+$fluid-spacing-s: 16px;
+$fluid-spacing-m: 24px;
+$fluid-spacing-l: 32px;
+$fluid-spacing-xl: 40px;
+$fluid-spacing-xxl: 48px;
+$fluid-spacing-giant: 64px;
+
 $fluid-border-radius: 3px;

+ 2 - 7
desktop/core/src/desktop/js/hue.js

@@ -57,9 +57,10 @@ import MultiLineEllipsisHandler from 'utils/multiLineEllipsisHandler';
 import sqlUtils from 'sql/sqlUtils';
 import sqlWorkerHandler from 'sql/sqlWorkerHandler';
 
+import 'components/sidebar/HueSidebarWebComponent';
+
 import 'ko/components/assist/assistViewModel';
 import OnePageViewModel from 'onePageViewModel';
-import SideBarViewModel from 'sideBarViewModel';
 import SidePanelViewModel from 'sidePanelViewModel';
 import TopNavViewModel from 'topNavViewModel';
 
@@ -134,12 +135,6 @@ $(document).ready(async () => {
   const topNavViewModel = new TopNavViewModel(onePageViewModel);
   ko.applyBindings(topNavViewModel, $('.top-nav')[0]);
 
-  const sidebarViewModel = new SideBarViewModel(onePageViewModel, topNavViewModel);
-  ko.applyBindings(sidebarViewModel, $('.hue-sidebar')[0]);
-  if (window.IS_MULTICLUSTER_ONLY) {
-    ko.applyBindings(sidebarViewModel, $('.hue-sidebar-container')[0]);
-  }
-
   $(document).on('hideHistoryModal', e => {
     $('#clearNotificationHistoryModal').modal('hide');
   });

+ 0 - 75
desktop/core/src/desktop/js/ko/bindings/ko.hueAppIcon.js

@@ -1,75 +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 * as ko from 'knockout';
-
-const APP_ICON_INDEX = {
-  abfs: `<svg class="hi hi-fw"><use xlink:href="#hi-adls"></use></svg>`,
-  adls: `<svg class="hi hi-fw"><use xlink:href="#hi-adls"></use></svg>`,
-  dashboard: `<svg class="hi hi-fw"><use xlink:href="#hi-dashboard"></use></svg>`,
-  default: `<i class="fa fa-fw fa-database"></i>`,
-  'dist-cp': `<i class="fa fa-fw fa-files-o"></i>`,
-  documents: `<svg class="hi hi-fw"><use xlink:href="#hi-documents"></use></svg>`,
-  editor: `<svg class="hi hi-fw"><use xlink:href="#hi-editor"></use></svg>`,
-  hbase: `<i class="fa fa-fw fa-th-large"></i>`,
-  hdfs: `<i class="fa fa-fw fa-files-o"></i>`,
-  hive: `<svg class="hi hi-fw"><use xlink:href="#hi-hive"></use></svg>`,
-  impala: `<svg class="hi hi-fw"><use xlink:href="#hi-impala"></use></svg>`,
-  importer: `<i class="fa fa-fw fa-cloud-upload"></i>`,
-  indexes: `<i class="fa fa-fw fa-search-plus"></i>`,
-  jar: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
-  java: `<i class="fa fa-fw fa-file-code-o"></i>`,
-  'job-designer': `<svg class="hi hi-fw"><use xlink:href="#hi-job-designer"></use></svg>`,
-  kafka: `<i class="fa fa-fw fa-sitemap"></i>`,
-  mapreduce: `<i class="fa fa-fw fa-file-archive-o"></i>`,
-  markdown: `<svg class="hi hi-fw"><use xlink:href="#hi-markdown"></use></svg>`,
-  notebook: `<svg class="hi hi-fw"><use xlink:href="#hi-file-notebook"></use></svg>`,
-  oozie: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
-  'oozie-bundle': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-bundle"></use></svg>`,
-  'oozie-coordinator': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-coordinator"></use></svg>`,
-  'oozie-workflow': `<svg class="hi hi-fw"><use xlink:href="#hi-oozie-workflow"></use></svg>`,
-  pig: `<svg class="hi hi-fw"><use xlink:href="#hi-pig"></use></svg>`,
-  py: `<svg class="hi hi-fw"><use xlink:href="#hi-py"></use></svg>`,
-  pyspark: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
-  queries: `<i class="fa fa-fw fa-tasks"></i>`,
-  r: `<svg class="hi hi-fw"><use xlink:href="#hi-r"></use></svg>`,
-  report: `<i class="fa fa-fw fa-area-chart"></i>`,
-  s3: `<i class="fa fa-fw fa-cubes"></i>`,
-  scala: `<svg class="hi hi-fw"><use xlink:href="#hi-scala"></use></svg>`,
-  scheduler: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
-  security: `<i class="fa fa-fw fa-lock"></i>`,
-  shell: `<i class="fa fa-fw fa-terminal"></i>`,
-  solr: `<i class="fa fa-fw fa-search-plus"></i>`,
-  spark2: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
-  spark: `<svg class="hi hi-fw"><use xlink:href="#hi-spark"></use></svg>`,
-  sqoop1: `<svg class="hi hi-fw"><use xlink:href="#hi-sqoop"></use></svg>`,
-  sqoop: `<svg class="hi hi-fw"><use xlink:href="#hi-sqoop"></use></svg>`,
-  support: `<svg class="hi hi-fw"><use xlink:href="#hi-support"></use></svg>`,
-  tables: `<i class="fa fa-fw fa-database"></i>`,
-  text: `<i class="fa fa-fw fa-i-cursor"></i>`,
-  warehouses: `<i class="altus-icon altus-adb-cluster" style="margin: 0 1px 0 3px"></i>`,
-  workflows: `<svg class="hi hi-fw"><use xlink:href="#hi-oozie"></use></svg>`,
-  yarn: `<i class="fa fa-fw fa-tasks"></i>`
-};
-
-ko.bindingHandlers.hueAppIcon = {
-  update: function (element, valueAccessor) {
-    const options = ko.unwrap(valueAccessor());
-    const iconName = options.icon ? ko.unwrap(options.icon) : options;
-    $(element).html(APP_ICON_INDEX[iconName] || APP_ICON_INDEX.default);
-  }
-};

+ 0 - 38
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.multiClusterSidebar.test.js.snap

@@ -1,38 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ko.multiClusterSidebar.js should render component 1`] = `
-"<div data-bind=\\"descendantsComplete: descendantsComplete, component: { name: &quot;hue-multi-cluster-sidebar&quot;, params: params }\\"><div class=\\"sidebar-content\\" data-bind=\\"foreach: categories\\">
-    <!-- ko if: typeof label !== 'undefined' --><!-- /ko -->
-    <!-- ko foreach: items -->
-    <!-- ko if: typeof items == 'undefined' -->
-    <a role=\\"button\\" class=\\"sidebar-item\\" data-bind=\\"css: { 'active': false }, hueLink: url, attr: { 'title': label }\\" title=\\"Data Warehouse\\">
-      <span class=\\"sidebar-icon without-tooltip\\">
-        <span data-bind=\\"css: icon\\" class=\\"altus-icon altus-adb-query\\"></span>
-      </span>
-      <span class=\\"sidebar-item-name\\" data-bind=\\"text: label\\">Data Warehouse</span>
-    </a>
-    <!-- /ko -->
-    <!-- ko if: typeof items !== 'undefined' --><!-- /ko -->
-    
-    <!-- ko if: typeof items == 'undefined' -->
-    <a role=\\"button\\" class=\\"sidebar-item\\" data-bind=\\"css: { 'active': false }, hueLink: url, attr: { 'title': label }\\" title=\\"Data Science\\">
-      <span class=\\"sidebar-icon without-tooltip\\">
-        <span data-bind=\\"css: icon\\" class=\\"altus-icon altus-ds\\"></span>
-      </span>
-      <span class=\\"sidebar-item-name\\" data-bind=\\"text: label\\">Data Science</span>
-    </a>
-    <!-- /ko -->
-    <!-- ko if: typeof items !== 'undefined' --><!-- /ko -->
-    
-    <!-- ko if: typeof items == 'undefined' -->
-    <a role=\\"button\\" class=\\"sidebar-item\\" data-bind=\\"css: { 'active': false }, hueLink: url, attr: { 'title': label }\\" title=\\"Admin\\">
-      <span class=\\"sidebar-icon without-tooltip\\">
-        <span data-bind=\\"css: icon\\" class=\\"altus-icon altus-de\\"></span>
-      </span>
-      <span class=\\"sidebar-item-name\\" data-bind=\\"text: label\\">Admin</span>
-    </a>
-    <!-- /ko -->
-    <!-- ko if: typeof items !== 'undefined' --><!-- /ko -->
-    <!-- /ko -->
-  </div></div>"
-`;

+ 0 - 228
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.sidebar.test.js.snap

@@ -1,228 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ko.sidebar.js should render component 1`] = `
-"<div data-bind=\\"descendantsComplete: descendantsComplete, component: { name: &quot;hue-sidebar&quot;, params: params }\\" class=\\"collapsed\\"><script type=\\"text/html\\" id=\\"sidebar-inner-item\\">
-    <!-- ko if: iconHtml -->
-    <div class=\\"icon\\" data-bind=\\"html: iconHtml\\"></div><span data-bind=\\"text: displayName\\"></span>
-    <!-- /ko -->
-    <!-- ko ifnot: iconHtml -->
-    <div class=\\"icon\\" data-bind=\\"hueAppIcon: icon\\"></div><span data-bind=\\"text: displayName\\"></span>
-    <!-- /ko -->
-  </script><script type=\\"text/html\\" id=\\"sidebar-sub-menu\\">
-    <div class=\\"sidebar-menu\\" data-bind=\\"
-        css: {
-          'open' : open() || hoverOpen(),
-          'fixed-bottom': fixedBottom
-        },
-        event: {
-          mouseenter: mouseEnter
-        }
-      \\">
-      <div class=\\"inner\\">
-        <div class=\\"menu\\">
-          <!-- ko if: headerTemplate -->
-          <div class=\\"menu-header\\" data-bind=\\"template: headerTemplate\\"></div>
-          <!-- /ko -->
-
-          <ul class=\\"sidebar-nav-list\\" data-bind=\\"foreach: children\\">
-            <li data-bind=\\"css: { 'divider': isDivider }\\">
-              <!-- ko if: isDivider -->
-                &nbsp;
-              <!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length -->
-                  <a href=\\"javascript:void(0);\\" data-bind=\\"toggle: open, text: displayName\\"></a>
-                  <!-- ko template: { name: 'sidebar-sub-menu' } --><!-- /ko -->
-                <!-- /ko -->
-                <!-- ko if: !children && url -->
-                  <a href=\\"javascript:void(0);\\" data-bind=\\"hueLink: url, text: displayName\\"></a>
-                <!-- /ko -->
-                <!-- ko if: !children && href -->
-                  <a href=\\"javascript:void(0);\\" target=\\"_blank\\" data-bind=\\"attr: { 'href': href }, text: displayName\\"></a>
-                <!-- /ko -->
-                <!-- ko if: !children && click -->
-                  <a href=\\"javascript:void(0);\\" target=\\"_blank\\" data-bind=\\"click: click.bind($data), text: displayName\\"></a>
-                <!-- /ko -->
-              <!-- /ko -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-  </script><script type=\\"text/html\\" id=\\"sidebar-item\\">
-    <div class=\\"item-wrapper\\" data-bind=\\"css: itemClass, event: { mouseenter: mouseEnter, mouseleave: mouseLeave }\\">
-      <!-- ko if: children && children.length -->
-        <a href=\\"javascript: void(0);\\" data-bind=\\"
-            toggle: open,
-            css: { 'active': active },
-            template: 'sidebar-inner-item'
-          \\" class=\\"item\\"></a>
-          <!-- ko template: 'sidebar-sub-menu' --><!-- /ko -->
-      <!-- /ko -->
-      <!-- ko if: !children || !children.length -->
-        <!-- ko if: click -->
-          <a href=\\"javascript: void(0);\\" data-bind=\\"
-              click: click,
-              attr: { 'aria-label': displayName, 'data-tooltip': displayName },
-              css: { 'active': active },
-              template: 'sidebar-inner-item'
-            \\" class=\\"item\\"></a>
-        <!-- /ko -->
-        <!-- ko ifnot: click -->
-          <a href=\\"javascript: void(0);\\" data-bind=\\"
-              hueLink: url,
-              publish: 'hue.sidebar.update.active',
-              attr: { 'aria-label': displayName, 'data-tooltip': displayName },
-              css: { 'active': active },
-              template: 'sidebar-inner-item'
-            \\" class=\\"item\\"></a>
-        <!-- /ko -->
-      <!-- /ko -->
-    </div>
-  </script><script type=\\"text/html\\" id=\\"user-header-template\\">
-    <div class=\\"user-icon\\" style=\\"background-color: #fff\\">F</div>
-    <div>
-      <div>foo</div>
-    </div>
-  </script><div class=\\"hue-sidebar-header\\" data-bind=\\"css: { 'hue-sidebar-custom-logo' : window.CUSTOM_LOGO }\\">
-    <a data-bind=\\"hueLink: '/home/'\\" href=\\"javascript: void(0);\\" title=\\"Documents\\">
-      <div class=\\"hue-sidebar-logo\\"><svg><use xlink:href=\\"#hi-sidebar-logo\\"></use></svg></div>
-    </a>
-  </div><div class=\\"hue-sidebar-body\\">
-    <!-- ko foreach: items --><!-- /ko -->
-  </div><div class=\\"hue-sidebar-footer\\">
-    <!-- ko foreach: footerItems -->
-    <!-- ko template: { name: 'sidebar-item' } -->
-    <div class=\\"item-wrapper\\" data-bind=\\"css: itemClass, event: { mouseenter: mouseEnter, mouseleave: mouseLeave }\\">
-      <!-- ko if: children && children.length -->
-        <a href=\\"javascript: void(0);\\" data-bind=\\"
-            toggle: open,
-            css: { 'active': active },
-            template: 'sidebar-inner-item'
-          \\" class=\\"item\\">
-    <!-- ko if: iconHtml --><!-- /ko -->
-    <!-- ko ifnot: iconHtml -->
-    <div class=\\"icon\\" data-bind=\\"hueAppIcon: icon\\"></div><span data-bind=\\"text: displayName\\">Support</span>
-    <!-- /ko -->
-  </a>
-          <!-- ko template: 'sidebar-sub-menu' -->
-    <div class=\\"sidebar-menu fixed-bottom\\" data-bind=\\"
-        css: {
-          'open' : open() || hoverOpen(),
-          'fixed-bottom': fixedBottom
-        },
-        event: {
-          mouseenter: mouseEnter
-        }
-      \\">
-      <div class=\\"inner\\">
-        <div class=\\"menu\\">
-          <!-- ko if: headerTemplate --><!-- /ko -->
-
-          <ul class=\\"sidebar-nav-list\\" data-bind=\\"foreach: children\\">
-            <li data-bind=\\"css: { 'divider': isDivider }\\">
-              <!-- ko if: isDivider --><!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length --><!-- /ko -->
-                <!-- ko if: !children && url --><!-- /ko -->
-                <!-- ko if: !children && href -->
-                  <a href=\\"https://docs.gethue.com\\" target=\\"_blank\\" data-bind=\\"attr: { 'href': href }, text: displayName\\">Documentation</a>
-                <!-- /ko -->
-                <!-- ko if: !children && click --><!-- /ko -->
-              <!-- /ko -->
-            </li>
-          
-            <li data-bind=\\"css: { 'divider': isDivider }\\">
-              <!-- ko if: isDivider --><!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length --><!-- /ko -->
-                <!-- ko if: !children && url --><!-- /ko -->
-                <!-- ko if: !children && href --><!-- /ko -->
-                <!-- ko if: !children && click -->
-                  <a href=\\"javascript:void(0);\\" target=\\"_blank\\" data-bind=\\"click: click.bind($data), text: displayName\\">Welcome Tour</a>
-                <!-- /ko -->
-              <!-- /ko -->
-            </li>
-          
-            <li data-bind=\\"css: { 'divider': isDivider }\\">
-              <!-- ko if: isDivider --><!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length --><!-- /ko -->
-                <!-- ko if: !children && url --><!-- /ko -->
-                <!-- ko if: !children && href -->
-                  <a href=\\"https://gethue.com\\" target=\\"_blank\\" data-bind=\\"attr: { 'href': href }, text: displayName\\">Gethue.com</a>
-                <!-- /ko -->
-                <!-- ko if: !children && click --><!-- /ko -->
-              <!-- /ko -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-  <!-- /ko -->
-      <!-- /ko -->
-      <!-- ko if: !children || !children.length --><!-- /ko -->
-    </div>
-  <!-- /ko -->
-    
-    <!-- ko template: { name: 'sidebar-item' } -->
-    <div class=\\"item-wrapper shepherd-user-menu\\" data-bind=\\"css: itemClass, event: { mouseenter: mouseEnter, mouseleave: mouseLeave }\\">
-      <!-- ko if: children && children.length -->
-        <a href=\\"javascript: void(0);\\" data-bind=\\"
-            toggle: open,
-            css: { 'active': active },
-            template: 'sidebar-inner-item'
-          \\" class=\\"item\\">
-    <!-- ko if: iconHtml -->
-    <div class=\\"icon\\" data-bind=\\"html: iconHtml\\"><div class=\\"user-icon\\">F</div></div><span data-bind=\\"text: displayName\\">foo</span>
-    <!-- /ko -->
-    <!-- ko ifnot: iconHtml --><!-- /ko -->
-  </a>
-          <!-- ko template: 'sidebar-sub-menu' -->
-    <div class=\\"sidebar-menu fixed-bottom\\" data-bind=\\"
-        css: {
-          'open' : open() || hoverOpen(),
-          'fixed-bottom': fixedBottom
-        },
-        event: {
-          mouseenter: mouseEnter
-        }
-      \\">
-      <div class=\\"inner\\">
-        <div class=\\"menu\\">
-          <!-- ko if: headerTemplate -->
-          <div class=\\"menu-header\\" data-bind=\\"template: headerTemplate\\">
-    <div class=\\"user-icon\\" style=\\"background-color: #fff\\">F</div>
-    <div>
-      <div>foo</div>
-    </div>
-  </div>
-          <!-- /ko -->
-
-          <ul class=\\"sidebar-nav-list\\" data-bind=\\"foreach: children\\">
-            <li data-bind=\\"css: { 'divider': isDivider }\\">
-              <!-- ko if: isDivider --><!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length --><!-- /ko -->
-                <!-- ko if: !children && url -->
-                  <a href=\\"javascript:void(0);\\" data-bind=\\"hueLink: url, text: displayName\\">Sign out</a>
-                <!-- /ko -->
-                <!-- ko if: !children && href --><!-- /ko -->
-                <!-- ko if: !children && click --><!-- /ko -->
-              <!-- /ko -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-  <!-- /ko -->
-      <!-- /ko -->
-      <!-- ko if: !children || !children.length --><!-- /ko -->
-    </div>
-  <!-- /ko -->
-    <!-- /ko -->
-    <a class=\\"hue-sidebar-trigger\\" data-bind=\\"toggle: collapsed\\">
-      <svg><use xlink:href=\\"#hi-collapse-nav\\"></use></svg>
-    </a>
-  </div></div>"
-`;

+ 0 - 136
desktop/core/src/desktop/js/ko/components/ko.multiClusterSidebar.js

@@ -1,136 +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 * as ko from 'knockout';
-
-import componentUtils from './componentUtils';
-import huePubSub from 'utils/huePubSub';
-import I18n from 'utils/i18n';
-
-export const NAME = 'hue-multi-cluster-sidebar';
-
-const TEMPLATE = `
-  <div class="sidebar-content" data-bind="foreach: categories">
-    <!-- ko if: typeof label !== 'undefined' -->
-    <h4 class="sidebar-category-item" data-bind="text: label"></h4>
-    <!-- /ko -->
-    <!-- ko foreach: items -->
-    <!-- ko if: typeof items == 'undefined' -->
-    <a role="button" class="sidebar-item" data-bind="css: { 'active': false }, hueLink: url, attr: { 'title': label }">
-      <span class="sidebar-icon without-tooltip">
-        <span data-bind="css: icon"></span>
-      </span>
-      <span class="sidebar-item-name" data-bind="text: label"></span>
-    </a>
-    <!-- /ko -->
-    <!-- ko if: typeof items !== 'undefined' -->
-    <a role="button" class="sidebar-item" href="javascript: void(0);" data-bind="sidebarSubmenuActivator, css: { 'active': false }, attr: {'title': label">
-      <span class="sidebar-icon without-tooltip">
-        <span data-bind="css: icon"></span>
-      </span>
-      <span class="sidebar-item-name" data-bind="text: label"></span>
-      <i class="submenu-icon fa fa-chevron-right"></i>
-    </a>
-    <div class="sidebar-submenu" style="display:none;" data-bind="foreach: items">
-      <a role="button" class="sidebar-submenu-item" data-bind="hueLink: url, attr: { 'title': label }, text: label"></a>
-    </div>
-    <!-- /ko -->
-    <!-- /ko -->
-  </div>
-`;
-
-ko.bindingHandlers.sidebarSubmenuActivator = {
-  init: function (element) {
-    const $element = $(element);
-    const $menu = $element.next();
-    let visible = false;
-    let hideTimeout = -1;
-
-    const show = function () {
-      window.clearTimeout(hideTimeout);
-      if (!visible) {
-        $menu.css({ top: $element.offset().top + 'px' });
-        huePubSub.publish('hue.sidebar.hide.submenus');
-        $menu.show();
-        visible = true;
-      }
-    };
-
-    const hide = function () {
-      hideTimeout = window.setTimeout(() => {
-        if (visible) {
-          $menu.hide();
-          visible = false;
-        }
-      }, 300);
-    };
-
-    const hideSub = huePubSub.subscribe('hue.sidebar.hide.submenus', () => {
-      window.clearTimeout(hideTimeout);
-      $menu.hide();
-      visible = false;
-    });
-
-    $element.on('click', show);
-    $element.on('mouseover', show);
-    $element.on('mouseout', hide);
-    $menu.on('mouseover', show);
-    $menu.on('mouseout', hide);
-
-    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
-      $menu.hide();
-      hideSub.remove();
-      $element.off('click');
-      $element.off('mouseover');
-      $element.off('mouseout');
-      $menu.off('mouseover');
-      $menu.off('mouseout');
-    });
-  }
-};
-
-// TODO: Fetch menu from the backend
-const FIXED_CATEGORIES = [
-  {
-    items: [
-      {
-        label: I18n('Data Warehouse'),
-        icon: 'altus-icon altus-adb-query',
-        url: '/editor?type=impala'
-      },
-      {
-        label: I18n('Data Science'),
-        url: '/',
-        icon: 'altus-icon altus-ds'
-      },
-      {
-        label: I18n('Admin'),
-        url: '/',
-        icon: 'altus-icon altus-de'
-      }
-    ]
-  }
-];
-
-class MultiClusterSidebar {
-  constructor() {
-    const self = this;
-    self.categories = ko.observableArray(FIXED_CATEGORIES);
-  }
-}
-
-componentUtils.registerComponent(NAME, MultiClusterSidebar, TEMPLATE);

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

@@ -1,557 +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 * as ko from 'knockout';
-
-import { ASSIST_ACTIVE_DB_CHANGED_EVENT, ASSIST_SET_DATABASE_EVENT } from './assist/events';
-import componentUtils from 'ko/components/componentUtils';
-import { GET_KNOWN_CONFIG_EVENT, CONFIG_REFRESHED_EVENT } from 'utils/hueConfig';
-import huePubSub from 'utils/huePubSub';
-import I18n from 'utils/i18n';
-import { withLocalStorage } from 'utils/storageUtils';
-
-export const NAME = 'hue-sidebar';
-
-const CLOSE_ON_NEW_HOVER_EVENT = 'hue.sidebar.close.on.new.hover';
-
-// prettier-ignore
-const TEMPLATE = `
-  <script type="text/html" id="sidebar-inner-item">
-    <!-- ko if: iconHtml -->
-    <div class="icon" data-bind="html: iconHtml"></div><span data-bind="text: displayName"></span>
-    <!-- /ko -->
-    <!-- ko ifnot: iconHtml -->
-    <div class="icon" data-bind="hueAppIcon: icon"></div><span data-bind="text: displayName"></span>
-    <!-- /ko -->
-  </script>
-
-  <script type="text/html" id="sidebar-sub-menu">
-    <div class="sidebar-menu" data-bind="
-        css: {
-          'open' : open() || hoverOpen(),
-          'fixed-bottom': fixedBottom
-        },
-        event: {
-          mouseenter: mouseEnter
-        }
-      ">
-      <div class="inner">
-        <div class="menu">
-          <!-- ko if: headerTemplate -->
-          <div class="menu-header" data-bind="template: headerTemplate"></div>
-          <!-- /ko -->
-
-          <ul class="sidebar-nav-list" data-bind="foreach: children">
-            <li data-bind="css: { 'divider': isDivider }">
-              <!-- ko if: isDivider -->
-                &nbsp;
-              <!-- /ko -->
-              <!-- ko ifnot: isDivider -->
-                <!-- ko if: children && children.length -->
-                  <a href="javascript:void(0);" data-bind="toggle: open, text: displayName"></a>
-                  <!-- ko template: { name: 'sidebar-sub-menu' } --><!-- /ko -->
-                <!-- /ko -->
-                <!-- ko if: !children && url -->
-                  <a href="javascript:void(0);" data-bind="hueLink: url, text: displayName"></a>
-                <!-- /ko -->
-                <!-- ko if: !children && href -->
-                  <a href="javascript:void(0);" target="_blank" data-bind="attr: { 'href': href }, text: displayName"></a>
-                <!-- /ko -->
-                <!-- ko if: !children && click -->
-                  <a href="javascript:void(0);" target="_blank" data-bind="click: click.bind($data), text: displayName"></a>
-                <!-- /ko -->
-              <!-- /ko -->
-            </li>
-          </ul>
-        </div>
-      </div>
-    </div>
-  </script>
-
-  <script type="text/html" id="sidebar-item">
-    <div class="item-wrapper" data-bind="css: itemClass, event: { mouseenter: mouseEnter, mouseleave: mouseLeave }">
-      <!-- ko if: children && children.length -->
-        <a href="javascript: void(0);" data-bind="
-            toggle: open,
-            css: { 'active': active },
-            template: 'sidebar-inner-item'
-          " class="item"></a>
-          <!-- ko template: 'sidebar-sub-menu' --><!-- /ko -->
-      <!-- /ko -->
-      <!-- ko if: !children || !children.length -->
-        <!-- ko if: click -->
-          <a href="javascript: void(0);" data-bind="
-              click: click,
-              attr: { 'aria-label': displayName, 'data-tooltip': displayName },
-              css: { 'active': active },
-              template: 'sidebar-inner-item'
-            " class="item"></a>
-        <!-- /ko -->
-        <!-- ko ifnot: click -->
-          <a href="javascript: void(0);" data-bind="
-              hueLink: url,
-              publish: 'hue.sidebar.update.active',
-              attr: { 'aria-label': displayName, 'data-tooltip': displayName },
-              css: { 'active': active },
-              template: 'sidebar-inner-item'
-            " class="item"></a>
-        <!-- /ko -->
-      <!-- /ko -->
-    </div>
-  </script>
-
-  <script type="text/html" id="user-header-template">
-    <div class="user-icon" style="background-color: #fff">${ window.LOGGED_USERNAME[0].toUpperCase() }</div>
-    <div>
-      <div>${ window.LOGGED_USERNAME }</div>
-    </div>
-  </script>
-
-  <!-- ko if: window.DISPLAY_APP_SWITCHER -->
-  <!-- ko component: { name: 'hue-app-switcher' } --><!-- /ko -->
-  <!-- /ko -->
-  <!-- ko ifnot: window.DISPLAY_APP_SWITCHER -->
-  <div class="hue-sidebar-header" data-bind="css: { 'hue-sidebar-custom-logo' : window.CUSTOM_LOGO }">
-    <a data-bind="hueLink: '/home/'" href="javascript: void(0);" title="${I18n('Documents')}">
-      <div class="hue-sidebar-logo"><svg><use xlink:href="#hi-sidebar-logo"></use></svg></div>
-    </a>
-  </div>
-  <!-- /ko -->
-  <div class="hue-sidebar-body">
-    <!-- ko foreach: items -->
-      <!-- ko if: isCategory -->
-        <!-- ko ifnot: $index() === 0 -->
-        <div class="item-spacer"></div>
-        <!-- /ko -->
-        <!-- ko template: {name: 'sidebar-item', foreach: children } --><!-- /ko -->
-      <!-- /ko -->
-      <!-- ko ifnot: isCategory -->
-        <!-- ko template: { name: 'sidebar-item' } --><!-- /ko -->
-      <!-- /ko -->
-    <!-- /ko -->
-  </div>
-  <div class="hue-sidebar-footer">
-    <!-- ko foreach: footerItems -->
-    <!-- ko template: { name: 'sidebar-item' } --><!-- /ko -->
-    <!-- /ko -->
-    <a class="hue-sidebar-trigger" data-bind="toggle: collapsed">
-      <svg><use xlink:href="#hi-collapse-nav"></use></svg>
-    </a>
-  </div>
-`;
-
-const trackCloseOnClick = (observable, id) => {
-  observable.subscribe(newVal => {
-    if (newVal) {
-      window.setTimeout(() => {
-        $(document).on('click.' + id, () => {
-          observable(false);
-        });
-      }, 0);
-    } else {
-      $(document).off('click.' + id);
-    }
-  });
-};
-
-class SidebarItem {
-  constructor(options) {
-    this.isCategory = !!options.isCategory;
-    this.displayName = options.displayName;
-    this.isDivider = !!options.isDivider;
-    this.href = options.href;
-    this.url = options.url;
-    this.icon = options.icon;
-    this.children = options.children;
-    this.name = options.name;
-    this.type = options.type;
-    this.active = ko.observable(false);
-    this.open = ko.observable(false);
-    this.hoverOpen = ko.observable(false);
-    this.click = options.click;
-    this.iconHtml = options.iconHtml;
-    this.itemClass = options.itemClass;
-    this.headerTemplate = options.headerTemplate;
-    this.fixedBottom = !!options.fixedBottom;
-
-    trackCloseOnClick(this.open, 'sidebar-sub');
-    trackCloseOnClick(this.hoverOpen, 'sidebar-sub');
-    this.hoverdelay = -1;
-
-    this.open.subscribe(() => {
-      huePubSub.publish(CLOSE_ON_NEW_HOVER_EVENT, this);
-      window.clearTimeout(this.hoverdelay);
-      this.hoverOpen(false);
-    });
-
-    huePubSub.subscribe(CLOSE_ON_NEW_HOVER_EVENT, item => {
-      if (item !== this) {
-        window.clearTimeout(this.hoverdelay);
-        this.hoverOpen(false);
-        this.open(false);
-      }
-    });
-  }
-
-  isActive(lastAppName) {
-    if (lastAppName === 'filebrowser') {
-      if (location.href.indexOf('=S3A') !== -1) {
-        return this.type === 's3';
-      } else if (location.href.indexOf('=adl') !== -1) {
-        return this.type === 'adls';
-      } else if (location.href.indexOf('=abfs') !== -1) {
-        return this.type === 'abfs';
-      }
-      return this.type === 'hdfs';
-    }
-
-    if (lastAppName && lastAppName === this.type) {
-      return true;
-    }
-
-    if (this.children && this.children.length) {
-      return this.children.some(child => child.isActive(lastAppName));
-    }
-
-    return location.pathname === '/hue' + this.url;
-  }
-
-  mouseEnter() {
-    if (this.open()) {
-      return;
-    }
-    huePubSub.publish(CLOSE_ON_NEW_HOVER_EVENT, this);
-    window.clearTimeout(this.hoverdelay);
-    this.hoverOpen(true);
-  }
-
-  mouseLeave() {
-    window.clearTimeout(this.hoverdelay);
-    this.hoverdelay = window.setTimeout(() => {
-      this.hoverOpen(false);
-    }, 400);
-  }
-}
-
-class Sidebar {
-  constructor(params, element) {
-    this.$element = $(element);
-
-    this.collapsed = ko.observable();
-    this.userMenuOpen = ko.observable(false);
-    this.supportMenuOpen = ko.observable(false);
-
-    trackCloseOnClick(this.userMenuOpen, 'userMenuOpen');
-
-    trackCloseOnClick(this.supportMenuOpen, 'supportMenuOpen');
-
-    this.collapsed.subscribe(newVal => {
-      if (newVal) {
-        this.$element.addClass('collapsed');
-      } else {
-        this.$element.removeClass('collapsed');
-      }
-    });
-
-    withLocalStorage('hue.sidebar.collapse', this.collapsed, true);
-
-    this.items = ko.observableArray();
-
-    const userChildren = [];
-
-    if (window.USER_VIEW_EDIT_USER_ENABLED) {
-      userChildren.push(
-        new SidebarItem({
-          url: '/useradmin/users/edit/' + window.LOGGED_USERNAME,
-          displayName: I18n('My Profile')
-        })
-      );
-    }
-
-    if (window.USER_IS_ADMIN || window.USER_IS_HUE_ADMIN) {
-      userChildren.push(
-        new SidebarItem({ url: '/useradmin/users/', displayName: I18n('Administer Users') })
-      );
-      userChildren.push(
-        new SidebarItem({
-          url: '/about/',
-          displayName: I18n('Administer Server'),
-          type: 'admin_wizard'
-        })
-      );
-    }
-
-    userChildren.push(new SidebarItem({ url: '/accounts/logout', displayName: I18n('Sign out') }));
-
-    this.footerItems = [
-      new SidebarItem({
-        displayName: 'Support',
-        icon: 'support',
-        children: [
-          new SidebarItem({
-            displayName: I18n('Documentation'),
-            href: 'https://docs.gethue.com'
-          }),
-          new SidebarItem({
-            displayName: I18n('Welcome Tour'),
-            click: () => {
-              huePubSub.publish('show.welcome.tour');
-            }
-          }),
-          new SidebarItem({
-            displayName: 'Gethue.com',
-            href: 'https://gethue.com'
-          })
-        ],
-        fixedBottom: true
-      }),
-      new SidebarItem({
-        displayName: window.LOGGED_USERNAME,
-        itemClass: 'shepherd-user-menu',
-        iconHtml: '<div class="user-icon">' + window.LOGGED_USERNAME[0].toUpperCase() + '</div>',
-        headerTemplate: 'user-header-template',
-        children: userChildren,
-        fixedBottom: true
-      })
-    ];
-    this.lastAppName = undefined;
-    this.lastHref = location.href;
-
-    const updateActive = () => {
-      this.userMenuOpen(false);
-      this.supportMenuOpen(false);
-      let activeFound = false;
-
-      const testActive = child => {
-        if (!activeFound) {
-          child.active(child.isActive(this.lastAppName));
-          activeFound = child.active();
-        } else {
-          child.active(false);
-        }
-      };
-
-      this.items().forEach(item => {
-        item.children.forEach(testActive);
-      });
-      this.footerItems.forEach(testActive);
-    };
-
-    const lastAssistDatabase = ko.observable();
-    const lastEditorDatabase = ko.observable();
-
-    huePubSub.subscribe(ASSIST_ACTIVE_DB_CHANGED_EVENT, lastAssistDatabase);
-    huePubSub.subscribe(ASSIST_SET_DATABASE_EVENT, lastEditorDatabase);
-
-    const configUpdated = clusterConfig => {
-      const items = [];
-
-      if (clusterConfig && clusterConfig.app_config) {
-        const favourite = clusterConfig.main_button_action;
-        const appsItems = [];
-        const appConfig = clusterConfig.app_config;
-
-        ['editor', 'dashboard', 'scheduler', 'sdkapps'].forEach(appName => {
-          const config = appConfig[appName];
-          if (window.CUSTOM_DASHBOARD_URL && appName === 'dashboard') {
-            appsItems.push(
-              new SidebarItem({
-                displayName: I18n('Dashboard'),
-                click: () => {
-                  window.open(window.CUSTOM_DASHBOARD_URL, '_blank');
-                },
-                icon: 'dashboard',
-                type: 'dashboard'
-              })
-            );
-            return;
-          }
-          if (config && config.interpreters.length) {
-            if (config.interpreters.length === 1) {
-              appsItems.push(
-                new SidebarItem({
-                  displayName: config.displayName,
-                  url: config.interpreters[0].page,
-                  icon: config.name,
-                  type: config.name
-                })
-              );
-            } else {
-              const subApps = [];
-              let lastWasSql = false;
-              let dividerAdded = false;
-              config.interpreters.forEach(interpreter => {
-                if (!dividerAdded && lastWasSql && !interpreter.is_sql) {
-                  subApps.push(new SidebarItem({ isDivider: true }));
-                  dividerAdded = true;
-                }
-                if (favourite && favourite.page === interpreter.page) {
-                  // Put the favourite on top
-                  subApps.unshift(
-                    new SidebarItem({
-                      displayName: interpreter.displayName,
-                      url: interpreter.page,
-                      icon: interpreter.dialect || interpreter.name,
-                      type: interpreter.dialect || interpreter.name
-                    })
-                  );
-                } else {
-                  subApps.push(
-                    new SidebarItem({
-                      displayName: interpreter.displayName,
-                      url: interpreter.page,
-                      icon: interpreter.dialect || interpreter.name,
-                      type: interpreter.dialect || interpreter.name
-                    })
-                  );
-                }
-                lastWasSql = interpreter.is_sql;
-              });
-
-              if (appName === 'editor' && window.SHOW_ADD_MORE_EDITORS) {
-                subApps.push(new SidebarItem({ isDivider: true }));
-                if (window.HAS_CONNECTORS) {
-                  subApps.push(
-                    new SidebarItem({
-                      displayName: I18n('Add more...'),
-                      url: '/desktop/connectors'
-                    })
-                  );
-                } else {
-                  subApps.push(
-                    new SidebarItem({
-                      displayName: I18n('Add more...'),
-                      href: 'https://docs.gethue.com/administrator/configuration/connectors/'
-                    })
-                  );
-                }
-              }
-              appsItems.push(
-                new SidebarItem({
-                  displayName: config.displayName,
-                  icon: config.name,
-                  type: config.name,
-                  children: subApps
-                })
-              );
-            }
-          }
-        });
-
-        if (appsItems.length > 0) {
-          items.push(
-            new SidebarItem({
-              isCategory: true,
-              displayName: I18n('Apps'),
-              children: appsItems
-            })
-          );
-        }
-
-        const browserItems = [];
-        if (appConfig.home) {
-          browserItems.push(
-            new SidebarItem({
-              displayName: appConfig.home.buttonName,
-              url: appConfig.home.page,
-              icon: 'documents',
-              type: appConfig.home.name
-            })
-          );
-        }
-        if (appConfig.browser && appConfig.browser.interpreters) {
-          appConfig.browser.interpreters.forEach(browser => {
-            if (browser.type === 'tables') {
-              browserItems.push(
-                new SidebarItem({
-                  displayName: browser.displayName,
-                  url: ko.pureComputed(() => {
-                    const dbDetails = lastEditorDatabase() || lastAssistDatabase();
-                    let url = browser.page;
-                    if (dbDetails) {
-                      if (dbDetails.name) {
-                        url += '/' + dbDetails.name;
-                      }
-                      url += '?connector_id=' + dbDetails.connector.id;
-                    }
-                    return url;
-                  }),
-                  icon: browser.type,
-                  type: browser.type
-                })
-              );
-            } else {
-              browserItems.push(
-                new SidebarItem({
-                  displayName: browser.displayName,
-                  url: browser.page,
-                  icon: browser.type,
-                  type: browser.type
-                })
-              );
-            }
-          });
-        }
-        if (browserItems.length > 0) {
-          items.push(
-            new SidebarItem({
-              isCategory: true,
-              displayName: appConfig.browser.displayName,
-              children: browserItems
-            })
-          );
-        }
-      }
-
-      this.items(items);
-      updateActive();
-    };
-
-    huePubSub.publish(GET_KNOWN_CONFIG_EVENT, configUpdated);
-    huePubSub.subscribe(CONFIG_REFRESHED_EVENT, configUpdated);
-
-    let throttle = -1;
-    huePubSub.subscribe('set.current.app.name', appName => {
-      if (!appName) {
-        return;
-      }
-      if (appName !== this.lastAppName || location.href !== this.lastHref) {
-        this.lastAppName = appName;
-        this.lastHref = location.href;
-        window.clearTimeout(throttle);
-        throttle = window.setTimeout(updateActive, 100);
-      }
-    });
-    updateActive();
-
-    huePubSub.subscribe('hue.sidebar.update.active', updateActive);
-  }
-
-  toggleCollapse() {
-    this.$element.toggleClass('collapsed');
-  }
-}
-
-componentUtils.registerComponent(
-  NAME,
-  {
-    createViewModel: function (params, componentInfo) {
-      return new Sidebar(params, componentInfo.element);
-    }
-  },
-  TEMPLATE
-);

+ 0 - 3
desktop/core/src/desktop/js/ko/ko.all.js

@@ -79,7 +79,6 @@ import 'ko/bindings/ko.highlight';
 import 'ko/bindings/ko.hiveChooser';
 import 'ko/bindings/ko.html';
 import 'ko/bindings/ko.hueach';
-import 'ko/bindings/ko.hueAppIcon';
 import 'ko/bindings/ko.hueCheckAll';
 import 'ko/bindings/ko.hueCheckbox';
 import 'ko/bindings/ko.hueChecked';
@@ -162,7 +161,6 @@ import 'ko/components/ko.historyPanel';
 import 'ko/components/ko.importDocumentsModal';
 import 'ko/components/ko.inlineAutocomplete';
 import 'ko/components/ko.jobBrowserLinks';
-import 'ko/components/ko.multiClusterSidebar';
 import 'ko/components/ko.navProperties';
 import 'ko/components/ko.navTags';
 import 'ko/components/ko.performanceGraph';
@@ -172,7 +170,6 @@ import 'ko/components/ko.sentryPrivileges';
 import 'ko/components/ko.sessionAuthModal';
 import 'ko/components/ko.sessionPanel';
 import 'ko/components/ko.shareGistModal';
-import 'ko/components/ko.sidebar';
 import 'ko/components/ko.sqlColumnsTable';
 
 // TODO: Move to about app when it has it's own webpack entry

+ 0 - 145
desktop/core/src/desktop/js/sideBarViewModel.js

@@ -1,145 +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 * as ko from 'knockout';
-
-import huePubSub from 'utils/huePubSub';
-import I18n from 'utils/i18n';
-import { CONFIG_REFRESHED_EVENT, GET_KNOWN_CONFIG_EVENT } from 'utils/hueConfig';
-
-class SideBarViewModel {
-  constructor(onePageViewModel, topNavViewModel) {
-    const self = this;
-
-    self.items = ko.observableArray();
-
-    self.pocClusterMode = topNavViewModel.pocClusterMode;
-
-    const configUpdated = 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({
-              displayName: I18n('Editor'),
-              url: editor['page'],
-              icon: 'editor'
-            });
-          } else {
-            appsItems.push({
-              displayName: appConfig['editor']['displayName'],
-              url: appConfig['editor']['page'],
-              icon: 'editor'
-            });
-          }
-        }
-        ['dashboard', 'scheduler'].forEach(appName => {
-          if (appConfig[appName]) {
-            appsItems.push({
-              displayName: appConfig[appName]['displayName'],
-              url: appConfig[appName]['page'],
-              icon: appName
-            });
-          }
-        });
-        if (appsItems.length > 0) {
-          items.push({
-            isCategory: true,
-            displayName: I18n('Apps'),
-            children: appsItems
-          });
-        }
-
-        const browserItems = [];
-        browserItems.push({
-          displayName: I18n('Documents'),
-          url: '/home/',
-          icon: 'documents'
-        });
-        if (appConfig['browser'] && appConfig['browser']['interpreters']) {
-          appConfig['browser']['interpreters'].forEach(browser => {
-            browserItems.push({
-              displayName: browser.displayName,
-              url: browser.page,
-              icon: browser.type
-            });
-          });
-        }
-        if (browserItems.length > 0) {
-          items.push({
-            isCategory: true,
-            displayName: I18n('Browsers'),
-            children: browserItems
-          });
-        }
-
-        const sdkItems = [];
-        if (appConfig['sdkapps'] && appConfig['sdkapps']['interpreters']) {
-          appConfig['sdkapps']['interpreters'].forEach(browser => {
-            sdkItems.push({
-              displayName: browser['displayName'],
-              url: browser['page']
-            });
-          });
-        }
-        if (sdkItems.length > 0) {
-          items.push({
-            isCategory: true,
-            displayName: appConfig['sdkapps']['displayName'],
-            children: sdkItems
-          });
-        }
-      }
-
-      self.items(items);
-    };
-
-    huePubSub.publish(GET_KNOWN_CONFIG_EVENT, configUpdated);
-    huePubSub.subscribe(CONFIG_REFRESHED_EVENT, configUpdated);
-
-    self.leftNavVisible = topNavViewModel.leftNavVisible;
-    self.onePageViewModel = onePageViewModel;
-  }
-}
-
-export default SideBarViewModel;

+ 3 - 1
desktop/core/src/desktop/js/types/config.ts

@@ -55,7 +55,8 @@ export enum AppType {
   browser = 'browser',
   editor = 'editor',
   dashboard = 'dashboard',
-  scheduler = 'scheduler'
+  scheduler = 'scheduler',
+  sdkapps = 'sdkapps'
 }
 
 export interface HueConfig extends GenericApiResponse {
@@ -64,6 +65,7 @@ export interface HueConfig extends GenericApiResponse {
     catalogs?: CatalogInterpreter[];
     [AppType.dashboard]?: AppConfig<DashboardInterpreter>;
     [AppType.editor]?: EditorConfig;
+    [AppType.sdkapps]?: AppConfig<Interpreter>;
     home?: AppConfig<Interpreter>;
     [AppType.scheduler]?: AppConfig<SchedulerInterpreter>;
   };

+ 6 - 0
desktop/core/src/desktop/js/types/types.ts

@@ -33,13 +33,19 @@ declare global {
 export interface hueWindow {
   CACHEABLE_TTL?: { default?: number; optimizer?: number };
   CLOSE_SESSIONS?: { [dialect: string]: boolean };
+  CUSTOM_DASHBOARD_URL?: string;
   HAS_CATALOG?: boolean;
+  HAS_CONNECTORS?: boolean;
   HAS_OPTIMIZER?: boolean;
   AUTOCOMPLETE_TIMEOUT?: number;
   ENABLE_SQL_SYNTAX_CHECK?: boolean;
   HUE_BASE_URL?: string;
   LOGGED_USERNAME?: string;
   OPTIMIZER_MODE?: string;
+  SHOW_ADD_MORE_EDITORS?: boolean;
+  USER_IS_ADMIN?: boolean;
+  USER_IS_HUE_ADMIN?: boolean;
+  USER_VIEW_EDIT_USER_ENABLED?: boolean;
   WEB_SOCKETS_ENABLED?: boolean;
   WS_CHANNEL?: string;
   hueDebug?: HueDebug;

+ 20 - 0
desktop/core/src/desktop/js/utils/hueUtils.ts

@@ -17,6 +17,7 @@
 import $ from 'jquery';
 import sanitizeHtml from 'sanitize-html';
 import { hueWindow } from 'types/types';
+import huePubSub from 'utils/huePubSub';
 import { hueLocalStorage, withLocalStorage } from './storageUtils';
 
 export const bootstrapRatios = {
@@ -576,6 +577,25 @@ export const stripHtmlFromFunctions = (template: string): string => {
   return stripped;
 };
 
+export const onHueLinkClick = (event: Event, url: string, target?: string): void => {
+  if (url.indexOf('http') === 0) {
+    window.open(url, target);
+  } else {
+    const prefix = (<hueWindow>window).HUE_BASE_URL + '/hue' + (url.indexOf('/') === 0 ? '' : '/');
+    if (target) {
+      window.open(prefix + url, target);
+    } else if (
+      (<KeyboardEvent>event).ctrlKey ||
+      (<KeyboardEvent>event).metaKey ||
+      (<KeyboardEvent>event).which === 2
+    ) {
+      window.open(prefix + url, '_blank');
+    } else {
+      huePubSub.publish('open.link', url);
+    }
+  }
+};
+
 export const sleep = async (timeout: number): Promise<void> =>
   new Promise(resolve => setTimeout(resolve, timeout));
 

+ 0 - 127
desktop/core/src/desktop/static/desktop/less/components/hue-multi-cluster-sidebar.less

@@ -1,127 +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: 180px;
-@collapsed-sidebar-width: 48px;
-@sidebar-animation-duration: 250ms;
-@sidebar-category-font-size: 13px;
-@sidebar-left-spacing: 16px;
-
-.hue-sidebar {
-  .sidebar-content {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    right: 0;
-    left: 0;
-    padding-top: 20px;
-    background-color: @cui-white;
-    overflow-y: auto;
-    overflow-x: hidden;
-
-    .sidebar-submenu {
-      position: fixed;
-      left: 182px;
-      min-width: 110px;
-      background-color: @cui-white;
-      border: 1px solid @cui-default-border-color;
-      .hue-box-shadow-right();
-
-      .sidebar-submenu-item {
-        display: block;
-        padding: 0 20px;
-        line-height: 36px;
-        color: @text-color;
-        white-space: nowrap;
-        position: relative;
-
-        &:hover {
-          color: @cui-blue-500;
-        }
-      }
-    }
-
-    .sidebar-item {
-      display: block;
-      line-height: 36px;
-      color: @text-color;
-      white-space: nowrap;
-      position: relative;
-
-      .sidebar-icon {
-        float: left;
-        padding: 0 14px 0 @sidebar-left-spacing;
-
-        &.with-tooltip {
-          display: none;
-        }
-      }
-
-      .fa,
-      .altus-icon {
-        width: 18px;
-        font-size: 16px;
-        text-align: center;
-        vertical-align: middle;
-      }
-
-      .submenu-icon {
-        position: absolute;
-        right: 3px;
-        top: 14px;
-        font-size: 11px;
-      }
-    }
-
-    .sidebar-item.active {
-      background: @cui-blue-050;
-      color: @cui-blue-500;
-
-      .fa::before {
-        color: @cui-blue-500;
-      }
-    }
-
-    .sidebar-item:hover {
-      color: @cui-blue-500;
-
-      .fa::before {
-        color: @cui-blue-500;
-      }
-    }
-
-    .sidebar-item:active {
-      background: @cui-blue-050;
-    }
-
-    .sidebar-category-item {
-      margin: 20px 0 4px @sidebar-left-spacing;
-      font-size: @sidebar-category-font-size;
-      font-weight: bold;
-      color: @cui-gray-600;
-      transition: opacity @sidebar-animation-duration;
-      white-space: nowrap;
-    }
-
-    .sidebar-item-name {
-      transition: opacity @sidebar-animation-duration;
-      opacity: 1;
-      vertical-align: middle;
-    }
-  }
-}

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

@@ -1,514 +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.
-*/
-
-@animation-duration-base: 0.2s;
-@gradient-end: #023259;
-@gradient-start: @cui-gray-800;
-@header-bg-open: @hue-primary-color-dark;
-@header-bg: @hue-primary-color-dark;
-@default-drawer-width: 400px;
-@item-active-border: @hue-primary-color-dark;
-@item-color: rgb(173, 178, 182);
-@sidebar-background-color: #333e45;
-@sidebar-collapsed-item-height: 35px;
-@sidebar-collapsed-icon-height: 22px;
-@sidebar-icon-height: 20px;
-@sidebar-collapsed-width: 56px;
-@sidebar-header-height: 56px;
-@sidebar-item-height: 32px;
-@sidebar-width: 248px;
-
-.svg-hue-sidebar-logo-main {
-  fill: currentColor !important;
-}
-
-.hue-sidebar {
-  .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;
-  transition: all @animation-duration-base;
-
-  .hi {
-    margin-bottom: 0;
-  }
-
-  * {
-    -webkit-font-smoothing: antialiased;
-    transition: all @animation-duration-base;
-
-    //box-sizing: border-box;
-    //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;
-
-    &[target='_blank']::after,
-    &[target='_blank']::before {
-      content: none !important;
-    }
-  }
-
-  .hue-sidebar-header {
-    .display-flex;
-    .flex-shrink(0);
-    .flex-box-align(center);
-    .flex-align-items(center);
-
-    height: @sidebar-header-height;
-    color: @cui-white;
-    font-size: 16px;
-    margin-bottom: 8px;
-    padding: 0 0 0 24px;
-    background-color: @header-bg;
-
-    &.hue-sidebar-custom-logo {
-      background-color: @cui-white;
-      border-top: 2px solid @hue-primary-color-dark;
-      height: calc(@sidebar-header-height - 3px); /* stylelint-disable-line */
-
-      .hue-sidebar-logo {
-        margin-left: 32px;
-      }
-
-      .hue-sidebar-logo svg {
-        width: 128px;
-      }
-    }
-
-    .hue-app-switcher-trigger {
-      color: @hue-primary-color-dark;
-      margin-right: 6px;
-
-      svg {
-        width: 20px;
-        height: 20px;
-      }
-    }
-
-    .hue-app-switcher-logo {
-      margin-left: 30px;
-      overflow-x: hidden;
-      color: @cui-white;
-
-      svg {
-        height: 32px;
-        width: 100px;
-      }
-    }
-
-    .title {
-      margin-left: 8px;
-      white-space: nowrap;
-      overflow: hidden;
-    }
-
-    .logo {
-      width: 36px;
-      height: 36px;
-    }
-
-    & > a {
-      .display-flex;
-      .flex-box-align(center);
-      .flex-box-pack(center);
-      .flex-align-items(center);
-
-      justify-content: center;
-    }
-
-    .hue-sidebar-logo {
-      margin-left: 49px;
-      height: 32px;
-      overflow-x: hidden;
-      color: @cui-white;
-    }
-
-    .hue-sidebar-logo svg {
-      height: 32px;
-      width: 100px;
-    }
-  }
-
-  .hue-sidebar-body {
-    .flex(1 1 auto);
-    .flex-box-flex(1);
-
-    overflow-y: hidden;
-  }
-
-  .item {
-    .display-flex;
-    .flex-align-items(center);
-
-    height: @sidebar-item-height;
-    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: bold;
-
-      &::after {
-        content: "";
-        position: absolute;
-        right: 0;
-        top: 0;
-        bottom: 0;
-        width: 4px;
-        background-color: @item-active-border;
-      }
-
-      .user-icon {
-        background-color: @cui-white;
-      }
-
-      .fa::before {
-        color: @cui-white;
-      }
-    }
-  }
-
-  .item-category {
-    margin: 20px 0 0 24px;
-    color: @cui-gray-500;
-    text-transform: uppercase;
-  }
-
-  a.item:hover:not(.active) {
-    color: @cui-white;
-  }
-
-  .icon {
-    .display-flex;
-    .flex(0 0 36px);
-    .flex-box-pack(center);
-
-    margin-left: 16px;
-    text-align: center;
-    justify-content: center;
-    font-size: @sidebar-icon-height;
-
-    i,
-    svg {
-      font-size: @sidebar-icon-height;
-      height: @sidebar-icon-height; // nv d3 conflict
-    }
-  }
-
-  .hue-sidebar-footer {
-    a.hue-sidebar-trigger {
-      .display-flex;
-      .flex-shrink(0);
-      .flex-align-items(center);
-      .flex-box-pack(end);
-
-      justify-content: flex-end;
-      height: 50px;
-      padding: 0 16px;
-
-      svg {
-        transition-delay: @animation-duration-base;
-        fill: @cui-gray-500;
-        width: 24px;
-        height: 24px;
-
-        &:hover {
-          fill: @cui-white;
-        }
-      }
-    }
-  }
-
-  .user-icon {
-    border-radius: 50%;
-    width: 20px;
-    height: 20px;
-    font-size: 13px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: @sidebar-background-color;
-    background-color: @item-color;
-    text-transform: uppercase;
-
-    &:hover {
-      background-color: @cui-white;
-    }
-  }
-
-  &.collapsed {
-    .flex-basis(@sidebar-collapsed-width);
-
-    .hue-sidebar-header {
-      padding: 0 12px;
-
-      .hue-sidebar-logo {
-        margin-left: 0;
-        width: 32px;
-        color: @cui-white;
-      }
-
-      &.hue-sidebar-custom-logo {
-        padding: 0;
-
-        .hue-sidebar-logo {
-          width: 56px;
-          margin-left: -6px;
-        }
-      }
-
-      .hue-app-switcher-trigger svg {
-        width: 32px;
-        height: 32px;
-      }
-
-      .hue-app-switcher-trigger + a {
-        .logo {
-          opacity: 0;
-        }
-      }
-
-      .logo {
-        margin: 0;
-        margin-left: -2px;
-      }
-
-      .title {
-        opacity: 0;
-      }
-    }
-
-    .item[data-tooltip] {
-      &::before {
-        z-index: 1010;
-        display: none;
-        content: attr(data-tooltip);
-        position: fixed;
-        left: @sidebar-collapsed-width + 8px;
-        background-color: @cui-gray-800;
-        color: @cui-white;
-        font-size: 12px;
-        border-radius: 3px;
-        box-shadow: rgba(0, 0, 0, 0.2) 0 2px 4px 0;
-        white-space: nowrap;
-        padding: 4px 8px;
-      }
-
-      &:hover::before {
-        display: block;
-      }
-    }
-
-    .item-category {
-      height: 0;
-      margin: 0;
-    }
-
-    .item {
-      height: @sidebar-collapsed-item-height;
-      padding: 0;
-
-      span {
-        opacity: 0;
-      }
-    }
-
-    .icon {
-      .flex(0 0 @sidebar-collapsed-width);
-      .flex-box-flex(0);
-
-      margin: 0;
-      font-size: @sidebar-collapsed-icon-height;
-      height: @sidebar-collapsed-icon-height;
-
-      i,
-      svg {
-        font-size: @sidebar-collapsed-icon-height;
-        height: @sidebar-collapsed-icon-height;
-      }
-    }
-
-    .hue-sidebar-footer a.hue-sidebar-trigger svg {
-      transform: rotate(180deg);
-    }
-
-    .user-icon {
-      font-weight: normal !important;
-      margin-top: -1px;
-      margin-left: -1px;
-      width: 23px;
-      height: 23px;
-      font-size: 16px;
-    }
-  }
-
-  &:not(.collapsed) .hue-sidebar-header {
-    background-color: @header-bg-open !important;
-
-    &.hue-sidebar-custom-logo {
-      background-color: @cui-white !important;
-    }
-  }
-
-  .item-spacer {
-    height: 15px;
-  }
-
-  &:not(.collapsed) .sidebar-menu {
-    left: @sidebar-width;
-  }
-
-  &.collapsed .sidebar-menu {
-    left: @sidebar-collapsed-width;
-  }
-
-  .sidebar-menu {
-    z-index: 1011;
-    position: absolute;
-    display: none;
-    margin-top: -40px;
-    height: auto;
-    padding-left: 8px;
-
-    .inner {
-      background-color: @cui-gray-800;
-      color: @cui-white;
-      font-size: 14px;
-      border-radius: 3px;
-      box-shadow: rgba(0, 0, 0, 0.2) 0 2px 4px 0;
-      white-space: nowrap;
-      padding: 4px 8px;
-      transition: none;
-    }
-
-    &.fixed-bottom {
-      margin-top: 0;
-      top: auto;
-      bottom: 20px;
-      position: fixed;
-    }
-
-    &.open {
-      display: block;
-    }
-
-    &.support-menu {
-      bottom: 38px;
-    }
-
-    .menu {
-      display: flex;
-      height: 100%;
-      flex-direction: column;
-
-      .menu-header {
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-        padding: 10px 16px 20px 16px;
-
-        .user-icon {
-          width: 36px;
-          height: 36px;
-          margin-right: 12px;
-          font-size: 22px;
-          flex-shrink: 0;
-        }
-
-        > div {
-          text-overflow: ellipsis;
-          white-space: nowrap;
-          overflow: hidden;
-        }
-
-        small {
-          font-size: 12px;
-        }
-      }
-
-      ul.sidebar-nav-list {
-        list-style: none;
-        margin: 4px 0;
-        padding: 0;
-
-        li {
-          margin-left: -8px;
-          margin-right: -8px;
-
-          &.divider {
-            height: 2px;
-            margin: 4px -8px;
-            background-color: #3e4b56;
-          }
-        }
-
-        li a {
-          height: 32px;
-          display: flex;
-          align-items: center;
-          color: @cui-white;
-          padding: 0 24px;
-
-          &:hover {
-            background-color: @hue-primary-color-dark;
-          }
-
-          svg {
-            margin-left: 8px;
-            font-size: 16px;
-          }
-        }
-      }
-
-      .menu-body {
-        flex-grow: 1;
-        min-height: 0;
-        overflow: auto;
-      }
-
-      .menu-footer {
-        flex-shrink: 0;
-
-        h2 {
-          padding: 24px 24px 0 24px;
-        }
-      }
-    }
-  }
-}

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

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

+ 1 - 1
desktop/core/src/desktop/templates/hue.mako

@@ -117,7 +117,7 @@ ${ hueIcons.symbols() }
 <input style="display:none" readonly autocomplete="false" type="password" name="fakepasswordremembered"/>
 
 <div class="hue-page">
-  <div class="hue-sidebar collapsed" data-bind="component: { name: 'hue-sidebar', params: {  } }"></div>
+  <hue-sidebar-web-component style="flex: 1 1 auto"></hue-sidebar-web-component>
 
   <div class="main-page">
     % if banner_message or conf.CUSTOM.BANNER_TOP_HTML.get():