Sfoglia il codice sorgente

[ui] Vue 3 - Migrated Sidebar components

sreenaths 4 anni fa
parent
commit
70543c98a0

+ 165 - 138
desktop/core/src/desktop/js/components/sidebar/AccordionItem.vue

@@ -62,10 +62,10 @@
       :aria-hidden="!isOpen || isCollapsed"
     >
       <AccordionSubItem
-        v-for="subItem in item.children"
-        :key="subItem.name"
+        v-for="(subItem, index) in item.children"
+        :key="index"
         :item="subItem"
-        :active="activeItemName === subItem.name"
+        :active="activeItemName === getItemName(subItem)"
         :disabled="!isOpen || isCollapsed"
       />
     </div>
@@ -106,10 +106,10 @@
         @scroll="onTooltipAccordionItemsScroll"
       >
         <AccordionSubItem
-          v-for="child in item.children"
-          :key="child.name"
+          v-for="(child, index) in item.children"
+          :key="index"
           :item="child"
-          :active="activeItemName === child.name"
+          :active="activeItemName === getItemName(child)"
         />
       </div>
     </BaseNavigationItemTooltip>
@@ -118,12 +118,11 @@
 <!-- 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 { defineComponent, PropType, inject } from 'vue';
+
   import AccordionSubItem from './AccordionSubItem.vue';
   import BaseNavigationItemTooltip from './BaseNavigationItemTooltip.vue';
-  import { SidebarAccordionItem, SidebarNavigationItem } from './types';
+  import { SidebarAccordionItem, SidebarNavigationItem, SidebarAccordionSubItem } from './types';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
 
   interface Tooltip {
@@ -134,69 +133,90 @@
     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)
-      );
-    }
+  export default defineComponent({
+    components: {
+      BaseNavigationItemTooltip,
+      AccordionSubItem
+    },
+
+    props: {
+      item: {
+        type: Object as PropType<SidebarAccordionItem>,
+        required: true
+      },
+      isCollapsed: Boolean,
+      activeItemName: {
+        type: String,
+        default: ''
+      }
+    },
 
-    get isUserMenu(): boolean {
-      return this.item.name === 'user';
-    }
+    setup(): {
+      subTracker: SubscriptionTracker;
+      selectedItemChanged?: (itemName: string) => void;
+    } {
+      return {
+        subTracker: new SubscriptionTracker(),
 
-    get accordionItemsHeight(): string {
-      const el = <HTMLElement>this.$refs.accordionItems;
-      if (this.isOpen && el) {
-        return `${el.scrollHeight}px`;
-      }
-      return '0';
-    }
+        selectedItemChanged: inject('selectedItemChanged')
+      };
+    },
 
-    get tooltipStyle(): CSSStyleDeclaration | undefined {
-      if (!this.tooltip) {
-        return {};
-      }
+    data(): {
+      isOpen: boolean;
+      isTooltipScrolled: boolean;
+      tooltip: Tooltip | null;
+    } {
+      return {
+        isOpen: false,
+        isTooltipScrolled: false,
+        tooltip: null
+      };
+    },
+
+    computed: {
+      isActive(): boolean {
+        return (
+          this.item.name === this.activeItemName ||
+          this.item.children.some(
+            item => (<SidebarNavigationItem>item).name === this.activeItemName
+          )
+        );
+      },
+      isUserMenu(): boolean {
+        return this.item.name === 'user';
+      },
+      accordionItemsHeight(): string {
+        const el = <HTMLElement>this.$refs.accordionItems;
+        if (this.isOpen && el) {
+          return `${el.scrollHeight}px`;
+        }
+        return '0';
+      },
+      tooltipStyle(): Partial<CSSStyleDeclaration> {
+        if (!this.tooltip) {
+          return {};
+        }
 
-      if (this.isCollapsed) {
-        // Prevent the menu from showing outside the window
-        const height = this.item.children.length * 32 + (this.isUserMenu ? 50 : 40);
-        const diff = this.tooltip.top + height - window.innerHeight;
-        if (diff > 0) {
-          return {
-            top: this.tooltip.top - diff - 5 + 'px',
-            left: this.tooltip.right + 'px'
-          };
+        if (this.isCollapsed) {
+          // Prevent the menu from showing outside the window
+          const height = this.item.children.length * 32 + (this.isUserMenu ? 50 : 40);
+          const diff = this.tooltip.top + height - window.innerHeight;
+          if (diff > 0) {
+            return {
+              top: this.tooltip.top - diff - 5 + 'px',
+              left: this.tooltip.right + 'px'
+            };
+          }
         }
+
+        return {
+          top: this.tooltip.top + 'px',
+          left: this.tooltip.right + 'px',
+          maxHeight: this.tooltip.maxHeight + 'px'
+        };
       }
-      return {
-        top: this.tooltip.top + 'px',
-        left: this.tooltip.right + 'px',
-        maxHeight: this.tooltip.maxHeight + 'px'
-      };
-    }
+    },
 
     mounted(): void {
       const containerEl = <HTMLElement>this.$refs.containerRef;
@@ -208,85 +228,92 @@
           });
         }
       }
-    }
+    },
 
-    destroyed(): void {
+    unmounted(): 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;
+    methods: {
+      getItemName(item: SidebarAccordionSubItem): string | undefined {
+        if (item.type === 'navigation') {
+          return item.name;
         }
-      }, 10);
-    }
-
-    onKeyPress(event: KeyboardEvent): void {
-      if ((event.key === 'Enter' || event.key === ' ') && this.isCollapsed) {
+      },
+      onFocusOut(): void {
         if (!this.tooltip) {
-          this.openTooltip(event.target as HTMLElement, true);
-        } else {
-          this.tooltip = null;
+          return;
         }
-      }
-    }
-
-    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;
-    }
-
-    onTooltipClick(): void {
-      this.tooltip = null;
-    }
-
-    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
-      };
-    }
+        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;
+          }
+        }
+      },
 
-    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);
+      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;
+      },
+
+      onTooltipClick(): void {
+        this.tooltip = null;
+      },
+
+      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>

+ 20 - 14
desktop/core/src/desktop/js/components/sidebar/AccordionSubItem.vue

@@ -34,22 +34,28 @@
 </template>
 
 <script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
   import SpacerItem from 'components/sidebar/SpacerItem.vue';
-  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: { SpacerItem, BaseNavigationItem }
-  })
-  export default class AccordionSubItem extends Vue {
-    @Prop()
-    item!: SidebarAccordionSubItem;
-    @Prop()
-    active!: boolean;
-    @Prop({ default: false })
-    disabled?: boolean;
-  }
+  export default defineComponent({
+    components: {
+      SpacerItem,
+      BaseNavigationItem
+    },
+
+    props: {
+      item: {
+        type: Object as PropType<SidebarAccordionSubItem>,
+        required: true
+      },
+      active: Boolean,
+      disabled: {
+        type: Boolean,
+        default: false
+      }
+    }
+  });
 </script>

+ 56 - 46
desktop/core/src/desktop/js/components/sidebar/BaseNavigationItem.vue

@@ -17,58 +17,68 @@
 -->
 
 <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>
+  <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>
 </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';
+  import { defineComponent, PropType, inject } from 'vue';
 
-  @Component({
-    components: { Fragment }
-  })
-  export default class BaseNavigationItem extends Vue {
-    @Inject()
-    selectedItemChanged?: (itemName: string) => void;
+  import { SidebarBaseItem } from './types';
 
-    @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);
+  export default defineComponent({
+    props: {
+      item: {
+        type: Object as PropType<SidebarBaseItem>,
+        required: true
+      },
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      cssClasses: {
+        type: String,
+        default: ''
       }
-      if (this.item.handler) {
-        this.item.handler(event);
+    },
+
+    emits: ['click'],
+
+    setup(): {
+      selectedItemChanged?: (itemName: string) => void;
+    } {
+      return {
+        selectedItemChanged: inject('selectedItemChanged')
+      };
+    },
+
+    methods: {
+      onClick(event: Event): void {
+        if (this.selectedItemChanged) {
+          this.selectedItemChanged(this.item.name);
+        }
+        if (this.item.handler) {
+          this.item.handler(event);
+        }
+        this.$emit('click');
       }
-      this.$emit('click');
     }
-  }
+  });
 </script>

+ 10 - 8
desktop/core/src/desktop/js/components/sidebar/BaseNavigationItemTooltip.vue

@@ -27,13 +27,15 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Prop } from 'vue-property-decorator';
+  import { defineComponent } from 'vue';
 
-  @Component
-  export default class BaseNavigationItemTooltip extends Vue {
-    @Prop({ default: false })
-    visible?: boolean;
-  }
+  export default defineComponent({
+    props: {
+      visible: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['click']
+  });
 </script>

+ 28 - 14
desktop/core/src/desktop/js/components/sidebar/HelpDrawerContent.vue

@@ -22,8 +22,12 @@
       <h2>Help</h2>
     </div>
     <ul>
-      <li v-for="childItem in children" :key="childItem.name">
-        <BaseNavigationItem :item="childItem" @click="hideDrawer">
+      <li v-for="(childItem, index) in children" :key="index">
+        <BaseNavigationItem
+          v-if="childItem.type === 'navigation'"
+          :item="childItem"
+          @click="hideDrawer"
+        >
           {{ childItem.displayName }}
         </BaseNavigationItem>
       </li>
@@ -32,20 +36,30 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Inject, Prop } from 'vue-property-decorator';
+  import { defineComponent, PropType, inject } from 'vue';
+
   import BaseNavigationItem from './BaseNavigationItem.vue';
   import { SidebarAccordionSubItem } from './types';
 
-  @Component({
-    components: { BaseNavigationItem }
-  })
-  export default class SmallHelpDrawerContent extends Vue {
-    @Inject()
-    hideDrawer!: () => void;
+  export default defineComponent({
+    components: {
+      BaseNavigationItem
+    },
+
+    props: {
+      children: {
+        type: Object as PropType<SidebarAccordionSubItem[]>,
+        required: false,
+        default: []
+      }
+    },
 
-    @Prop({ required: false, default: [] })
-    children?: SidebarAccordionSubItem[];
-  }
+    setup(): {
+      hideDrawer?: () => void;
+    } {
+      return {
+        hideDrawer: inject('hideDrawer')
+      };
+    }
+  });
 </script>

+ 296 - 262
desktop/core/src/desktop/js/components/sidebar/HueSidebar.vue

@@ -34,11 +34,7 @@
 </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 { defineComponent } from 'vue';
   import {
     HelpDrawerItem,
     SidebarAccordionItem,
@@ -47,6 +43,9 @@
     SidebarNavigationItem,
     UserDrawerItem
   } from './types';
+
+  import Sidebar from './Sidebar.vue';
+
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import {
     ASSIST_ACTIVE_DB_CHANGED_EVENT,
@@ -180,41 +179,71 @@
     }
   ];
 
-  @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);
-    }
+  export default defineComponent({
+    components: {
+      Sidebar
+    },
 
-    onHeaderClick(event: MouseEvent): void {
-      onHueLinkClick(event, '/home');
-    }
+    provide(): {
+      selectedItemChanged: (itemName: string) => void;
+    } {
+      return {
+        selectedItemChanged: (itemName: string): void => {
+          if (
+            itemName !== 'user' &&
+            itemName !== 'help' &&
+            itemName !== 'sidebar-collapse-btn' &&
+            this.activeItemName !== itemName
+          ) {
+            this.activeItemName = itemName;
+          }
+        }
+      };
+    },
+
+    setup(): {
+      userDrawerItem: UserDrawerItem;
+      userDrawerChildren: SidebarAccordionSubItem[];
+
+      helpDrawerItem: HelpDrawerItem;
+      helpDrawerChildren: SidebarAccordionSubItem[];
+
+      lastEditorDatabase?: EditorDatabaseDetails;
+      lastAssistDatabase?: AssistDatabaseDetails;
+
+      subTracker: SubscriptionTracker;
+    } {
+      return {
+        userDrawerItem: {
+          displayName: (<hueWindow>window).LOGGED_USERNAME || '',
+          logoutLabel: I18n('Log Out'),
+          logoutHandler: (event: Event) => onHueLinkClick(event, '/accounts/logout')
+        },
+        userDrawerChildren: USER_DRAWER_CHILDREN,
+
+        helpDrawerItem: {
+          displayName: I18n('Help'),
+          iconHtml: getIconHtml('support')
+        },
+        helpDrawerChildren: HELP_DRAWER_CHILDREN,
+
+        subTracker: new SubscriptionTracker()
+      };
+    },
+
+    data(): {
+      sidebarItems: SidebarItem[];
+      activeItemName: string;
+      isCollapsed: boolean;
+      drawerTopic: string | null;
+    } {
+      return {
+        sidebarItems: [],
+        activeItemName: '',
+        isCollapsed: getFromLocalStorage('hue.sidebar.collapse', true),
+        drawerTopic: null
+      };
+    },
 
     mounted(): void {
       const config = getLastKnownConfig();
@@ -236,252 +265,257 @@
 
       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;
-
-      const params = new URLSearchParams(location.search);
-      switch (appName) {
-        case 'dashboard':
-          adaptedName = params.get('engine') ? `${appName}-${params.get('engine')}` : appName;
-          break;
-        case 'editor':
-          adaptedName = params.get('type') ? `${appName}-${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 = 'scheduler-oozie-bundle';
-          break;
-        case 'oozie_coordinator':
-          adaptedName = 'scheduler-oozie-coordinator';
-          break;
-        case 'oozie_workflow':
-          adaptedName = 'scheduler-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 'hbase':
-        case 'importer':
-        case 'indexes':
-        case 'kafka':
-          break;
-        default:
-          console.warn('No sidebar alternative for app: ' + appName);
-      }
+    unmounted(): void {
+      this.subTracker.dispose();
+    },
 
-      if (this.activeItemName !== adaptedName) {
-        this.identifyActive(adaptedName || appName);
-      }
-    }
+    methods: {
+      onHeaderClick(event: MouseEvent): void {
+        onHueLinkClick(event, '/home');
+      },
+      toggleCollapsed(): void {
+        this.isCollapsed = !this.isCollapsed;
+        setInLocalStorage('hue.sidebar.collapse', this.isCollapsed);
+      },
+      currentAppChanged(appName: string): void {
+        let adaptedName;
+
+        const params = new URLSearchParams(location.search);
+        switch (appName) {
+          case 'dashboard':
+            adaptedName = params.get('engine') ? `${appName}-${params.get('engine')}` : appName;
+            break;
+          case 'editor':
+            adaptedName = params.get('type') ? `${appName}-${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 = 'scheduler-oozie-bundle';
+            break;
+          case 'oozie_coordinator':
+            adaptedName = 'scheduler-oozie-coordinator';
+            break;
+          case 'oozie_workflow':
+            adaptedName = 'scheduler-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 'hbase':
+          case 'importer':
+          case 'indexes':
+          case 'kafka':
+            break;
+          default:
+            console.warn('No sidebar alternative for app: ' + appName);
+        }
 
-    destroyed(): void {
-      this.subTracker.dispose();
-    }
+        if (this.activeItemName !== adaptedName) {
+          this.identifyActive(adaptedName || appName);
+        }
+      },
 
-    identifyActive(possibleItemName: string): void {
-      if (!possibleItemName) {
-        return;
-      }
+      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;
-          }
+        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;
-        });
-        return found;
-      };
+        };
 
-      const foundName = findInside(this.sidebarItems);
-      if (foundName) {
-        this.activeItemName = foundName;
-      }
-    }
+        const foundName = findInside(this.sidebarItems);
+        if (foundName) {
+          this.activeItemName = foundName;
+        }
+      },
 
-    hueConfigUpdated(clusterConfig: HueConfig): void {
-      const items: SidebarItem[] = [];
+      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;
+        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];
+          [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');
+              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: `${appName}-${config.name}`,
+                    displayName: config.displayName,
+                    iconHtml: getIconHtml(config.name),
+                    url: config.page,
+                    handler: (event: Event) => onHueLinkClick(event, <string>config.page)
+                  });
+                } else {
+                  const subApps: SidebarAccordionSubItem[] = [];
+                  config.interpreters.forEach(interpreter => {
+                    const interpreterItem: SidebarAccordionSubItem = {
+                      type: 'navigation',
+                      name: `${appName}-${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);
+                    }
+                  });
+
+                  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: 'editor-AddMoreInterpreters',
+                      displayName: I18n('Edit list...'),
+                      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')
             });
-            return;
           }
-          if (config && config.interpreters.length) {
-            if (config.interpreters.length === 1) {
-              appsItems.push({
-                type: 'navigation',
-                name: `${appName}-${config.name}`,
-                displayName: config.displayName,
-                iconHtml: getIconHtml(config.name),
-                url: config.page,
-                handler: (event: Event) => onHueLinkClick(event, <string>config.page)
-              });
-            } else {
-              const subApps: SidebarAccordionSubItem[] = [];
-              config.interpreters.forEach(interpreter => {
-                const interpreterItem: SidebarAccordionSubItem = {
+          if (appConfig.browser && appConfig.browser.interpreters) {
+            appConfig.browser.interpreters.forEach(browser => {
+              if (browser.type === 'tables') {
+                browserItems.push({
                   type: 'navigation',
-                  name: `${appName}-${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);
-                }
-              });
-
-              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({
+                  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: 'editor-AddMoreInterpreters',
-                  displayName: I18n('Edit list...'),
-                  url,
-                  handler: (event: Event) => onHueLinkClick(event, url)
+                  name: browser.type,
+                  displayName: browser.displayName,
+                  url: browser.page,
+                  handler: (event: Event) => onHueLinkClick(event, browser.page),
+                  iconHtml: getIconHtml(browser.type)
                 });
               }
-              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' });
             }
-          });
-        }
-        if (browserItems.length) {
-          if (items.length) {
-            items.push({ type: 'spacer' });
-          }
 
-          items.push(...browserItems);
-        }
+            items.push(...browserItems);
+          }
 
-        this.sidebarItems = items;
-        if (!this.activeItemName) {
-          huePubSub.publish('get.current.app.name', this.currentAppChanged.bind(this));
+          this.sidebarItems = items;
+          if (!this.activeItemName) {
+            huePubSub.publish('get.current.app.name', this.currentAppChanged.bind(this));
+          }
         }
       }
     }
-  }
+  });
 </script>
+
+<style lang="scss">
+  @import './hueSidebar.scss';
+</style>

+ 1 - 1
desktop/core/src/desktop/js/components/sidebar/HueSidebarWebComponent.ts

@@ -15,6 +15,6 @@
 // limitations under the License.
 
 import HueSidebar from './HueSidebar.vue';
-import { wrap } from 'vue/webComponentWrapper';
+import { wrap } from 'vue/webComponentWrap';
 
 wrap('hue-sidebar-web-component', HueSidebar);

+ 59 - 35
desktop/core/src/desktop/js/components/sidebar/NavigationItem.vue

@@ -49,52 +49,76 @@
 <!-- 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 { defineComponent, PropType } from 'vue';
+
   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;
-    }
+  export default defineComponent({
+    components: {
+      BaseNavigationItemTooltip,
+      BaseNavigationItem
+    },
+
+    props: {
+      isCollapsed: Boolean,
+      item: {
+        type: Object as PropType<SidebarNavigationItem>,
+        required: true
+      },
+      activeItemName: {
+        type: String,
+        default: ''
+      }
+    },
 
-    tooltip: DOMRect | null = null;
+    setup(): {
+      subTracker: SubscriptionTracker;
+    } {
+      return {
+        subTracker: new SubscriptionTracker()
+      };
+    },
+
+    data(): {
+      tooltip: DOMRect | null;
+    } {
+      return {
+        tooltip: null
+      };
+    },
+
+    computed: {
+      isActive(): boolean {
+        return this.item.name === this.activeItemName;
+      }
+    },
 
     mounted(): void {
-      this.subTracker.addEventListener(<HTMLElement>this.$parent.$el, 'scroll', () => {
-        this.tooltip = null;
-      });
-    }
+      if (this.$parent) {
+        this.subTracker.addEventListener(<HTMLElement>this.$parent.$el, 'scroll', () => {
+          this.tooltip = null;
+        });
+      }
+    },
 
-    destroyed(): void {
+    unmounted(): void {
       this.subTracker.dispose();
-    }
+    },
 
-    showTooltip(event: MouseEvent): void {
-      if (!this.isCollapsed) {
-        return;
-      }
-      this.tooltip = (event.target as HTMLElement).getBoundingClientRect();
-    }
+    methods: {
+      showTooltip(event: MouseEvent): void {
+        if (!this.isCollapsed) {
+          return;
+        }
+        this.tooltip = (event.target as HTMLElement).getBoundingClientRect();
+      },
 
-    hideTooltip(): void {
-      this.tooltip = null;
+      hideTooltip(): void {
+        this.tooltip = null;
+      }
     }
-  }
+  });
 </script>

+ 18 - 12
desktop/core/src/desktop/js/components/sidebar/SectionItem.vue

@@ -21,18 +21,24 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Prop } from 'vue-property-decorator';
+  import { defineComponent, PropType } from 'vue';
+
   import { SidebarSectionItem } from './types';
 
-  @Component
-  export default class SectionItem extends Vue {
-    @Prop()
-    activeItemName?: string;
-    @Prop()
-    isCollapsed!: boolean;
-    @Prop()
-    item!: SidebarSectionItem;
-  }
+  export default defineComponent({
+    props: {
+      activeItemName: {
+        type: String,
+        default: ''
+      },
+      isCollapsed: {
+        type: Boolean,
+        required: false
+      },
+      item: {
+        type: Object as PropType<SidebarSectionItem>,
+        required: true
+      }
+    }
+  });
 </script>

+ 187 - 148
desktop/core/src/desktop/js/components/sidebar/Sidebar.vue

@@ -17,98 +17,91 @@
 -->
 
 <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 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);" @click="$emit('header-click', $event)">
+        <svg>
+          <use xlink:href="#hi-sidebar-logo" />
+        </svg>
+      </a>
     </div>
-    <div class="sidebar" :class="{ 'sidebar-collapsed': isCollapsed }">
-      <div class="sidebar-header">
-        <a href="javascript:void(0);" @click="$emit('header-click', $event)">
-          <svg>
-            <use xlink:href="#hi-sidebar-logo" />
-          </svg>
-        </a>
-      </div>
-      <SidebarBody
-        :items="sidebarItems"
+    <SidebarBody
+      :items="sidebarItems"
+      :is-collapsed="isCollapsed"
+      :active-item-name="activeItemName"
+    />
+    <div class="sidebar-footer">
+      <NavigationItem
+        v-if="helpItem && useDrawerForHelp"
+        :item="helpItem"
+        :is-collapsed="isCollapsed"
+        :active-item-name="activeItemName"
+      />
+      <AccordionItem
+        v-if="helpItem && !useDrawerForHelp"
+        :item="helpItem"
+        :is-collapsed="isCollapsed"
+        :active-item-name="activeItemName"
+      />
+      <NavigationItem
+        v-if="userItem && useDrawerForUser"
+        :item="userItem"
+        :is-collapsed="isCollapsed"
+        :active-item-name="activeItemName"
+      />
+      <AccordionItem
+        v-if="userItem && !useDrawerForUser"
+        :item="userItem"
         :is-collapsed="isCollapsed"
         :active-item-name="activeItemName"
       />
-      <div class="sidebar-footer">
-        <NavigationItem
-          v-if="helpItem && useDrawerForHelp"
-          :item="helpItem"
-          :is-collapsed="isCollapsed"
-          :active-item-name="activeItemName"
-        />
-        <AccordionItem
-          v-if="helpItem && !useDrawerForHelp"
-          :item="helpItem"
-          :is-collapsed="isCollapsed"
-          :active-item-name="activeItemName"
-        />
-        <NavigationItem
-          v-if="userItem && useDrawerForUser"
-          :item="userItem"
-          :is-collapsed="isCollapsed"
-          :active-item-name="activeItemName"
-        />
-        <AccordionItem
-          v-if="userItem && !useDrawerForUser"
-          :item="userItem"
-          :is-collapsed="isCollapsed"
-          :active-item-name="activeItemName"
-        />
-        <div class="sidebar-footer-bottom-row">
-          <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 class="sidebar-footer-bottom-row">
+        <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>
-  </Fragment>
+  </div>
 </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 { defineComponent, PropType } from 'vue';
 
   import AccordionItem from './AccordionItem.vue';
   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 {
@@ -121,97 +114,143 @@
   } from './types';
   import UserDrawerContent from './UserDrawerContent.vue';
 
-  @Component({
+  export default defineComponent({
     components: {
       AccordionItem,
       HelpDrawerContent,
       UserDrawerContent,
       SidebarDrawer,
-      Fragment,
       NavigationItem,
       BaseNavigationItem,
       SidebarBody
-    }
-  })
-  export default class Sidebar extends Vue {
-    @Prop()
-    sidebarItems!: SidebarItem[];
+    },
 
-    @Prop({ required: false, default: true })
-    useDrawerForUser?: boolean;
-    @Prop({ required: false })
-    userDrawerItem: UserDrawerItem | null = null;
-    @Prop({ required: false })
-    userDrawerChildren: SidebarAccordionSubItem[] = [];
+    props: {
+      sidebarItems: {
+        type: Object as PropType<SidebarItem[]>,
+        required: true
+      },
+      useDrawerForUser: {
+        type: Boolean,
+        default: true
+      },
+      userDrawerItem: {
+        type: Object as PropType<UserDrawerItem | null>,
+        default: null
+      },
+      userDrawerChildren: {
+        type: Object as PropType<SidebarAccordionSubItem[]>,
+        default: []
+      },
+      useDrawerForHelp: {
+        type: Boolean,
+        default: true
+      },
+      helpDrawerItem: {
+        type: Object as PropType<HelpDrawerItem | null>,
+        default: null
+      },
+      helpDrawerChildren: {
+        type: Object as PropType<SidebarAccordionSubItem[]>,
+        default: []
+      },
 
-    @Prop({ required: false, default: true })
-    useDrawerForHelp?: boolean;
-    @Prop({ required: false })
-    helpDrawerItem: HelpDrawerItem | null = null;
-    @Prop({ required: false })
-    helpDrawerChildren: SidebarAccordionSubItem[] = [];
+      activeItemName: {
+        type: String,
+        required: true
+      },
+      isCollapsed: {
+        type: Boolean,
+        required: true,
+        default: true
+      },
+      drawerTopic: {
+        type: Object as PropType<string | null>,
+        default: null
+      }
+    },
 
-    @Prop({ required: false })
-    activeItemName?: string;
-    @Prop({ required: false, default: true })
-    isCollapsed?: boolean;
+    emits: ['toggle-collapsed', 'header-click'],
 
-    drawerTopic: string | null = null;
+    data(
+      props
+    ): {
+      drawerTopicInternal: string | null;
+    } {
+      return {
+        drawerTopicInternal: props.drawerTopic
+      };
+    },
 
-    closeDrawer(topic: string): void {
-      if (this.drawerTopic === topic) {
-        this.drawerTopic = null;
-      }
-    }
+    computed: {
+      helpItem(): SidebarAccordionItem | SidebarNavigationItem | null {
+        if (this.helpDrawerItem != null) {
+          const sharedProps = {
+            name: 'help',
+            displayName: this.helpDrawerItem.displayName,
+            iconHtml: this.helpDrawerItem.iconHtml
+          };
+          if (this.useDrawerForHelp) {
+            return {
+              type: 'navigation',
+              ...sharedProps,
+              handler: () => {
+                this.drawerTopicInternal = 'help';
+              }
+            };
+          }
+          return {
+            type: 'accordion',
+            ...sharedProps,
+            children: this.helpDrawerChildren
+          };
+        }
 
-    get helpItem(): SidebarAccordionItem | SidebarNavigationItem | null {
-      if (!this.helpDrawerItem) {
         return null;
-      }
-      const sharedProps = {
-        name: 'help',
-        displayName: this.helpDrawerItem.displayName,
-        iconHtml: this.helpDrawerItem.iconHtml
-      };
-      if (this.useDrawerForHelp) {
+      },
+      userItem(): SidebarAccordionItem | SidebarNavigationItem | null {
+        if (!this.userDrawerItem) {
+          return null;
+        }
+        const sharedProps = {
+          name: 'user',
+          displayName: this.userDrawerItem.displayName,
+          iconHtml: `<div class="sidebar-user-icon" role="img">${this.userDrawerItem.displayName[0].toUpperCase()}</div>`
+        };
+        if (this.useDrawerForUser) {
+          return {
+            type: 'navigation',
+            ...sharedProps,
+            handler: () => {
+              this.drawerTopicInternal = 'user';
+            }
+          };
+        }
         return {
-          type: 'navigation',
+          type: 'accordion',
           ...sharedProps,
-          handler: () => {
-            this.drawerTopic = 'help';
-          }
+          children: this.userDrawerChildren
         };
       }
-      return {
-        type: 'accordion',
-        ...sharedProps,
-        children: this.helpDrawerChildren
-      };
-    }
+    },
 
-    get userItem(): SidebarAccordionItem | SidebarNavigationItem | null {
-      if (!this.userDrawerItem) {
-        return null;
+    watch: {
+      drawerTopic(value: string | null): void {
+        this.drawerTopicInternal = value;
       }
-      const sharedProps = {
-        name: 'user',
-        displayName: this.userDrawerItem.displayName,
-        iconHtml: `<div class="sidebar-user-icon" role="img">${this.userDrawerItem.displayName[0].toUpperCase()}</div>`
-      };
-      if (this.useDrawerForUser) {
-        return {
-          type: 'navigation',
-          ...sharedProps,
-          handler: () => {
-            this.drawerTopic = 'user';
-          }
-        };
+    },
+
+    methods: {
+      closeDrawer(topic: string): void {
+        if (this.drawerTopicInternal === topic) {
+          this.drawerTopicInternal = null;
+        }
       }
-      return {
-        type: 'accordion',
-        ...sharedProps,
-        children: this.userDrawerChildren
-      };
     }
-  }
+  });
 </script>
+
+<style lang="scss">
+  @import './drawer.scss';
+  @import './sidebar.scss';
+</style>

+ 59 - 44
desktop/core/src/desktop/js/components/sidebar/SidebarBody.vue

@@ -55,9 +55,8 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Prop, Watch } from 'vue-property-decorator';
+  import { defineComponent, PropType } from 'vue';
+
   import { overflowOnHover } from 'components/directives/overflowOnHoverDirective';
   import AccordionItem from './AccordionItem.vue';
   import NavigationItem from './NavigationItem.vue';
@@ -66,54 +65,70 @@
   import { SidebarItem } from './types';
   import { defer } from 'utils/hueUtils';
 
-  @Component({
+  export default defineComponent({
     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);
-      });
-    }
+    props: {
+      items: {
+        type: Object as PropType<SidebarItem[]>,
+        required: true
+      },
+      isCollapsed: {
+        type: Boolean,
+        required: false
+      },
+      activeItemName: {
+        type: String,
+        default: ''
+      }
+    },
 
-    onScroll(evt: Event): void {
-      const el = <HTMLElement | null>evt.target;
-      this.detectOverflow(el);
-    }
+    data(): {
+      showOverflowIndicatorTop: boolean;
+      showOverflowIndicatorBtm: boolean;
+    } {
+      return {
+        showOverflowIndicatorTop: false,
+        showOverflowIndicatorBtm: false
+      };
+    },
 
-    detectOverflow(el?: HTMLElement | null): void {
-      if (!el) {
-        return;
+    watch: {
+      items(): void {
+        defer(() => {
+          this.detectOverflow(<HTMLElement>this.$refs.sidebarNav);
+        });
       }
-      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;
+    },
+
+    methods: {
+      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>

+ 37 - 23
desktop/core/src/desktop/js/components/sidebar/SidebarDrawer.vue

@@ -23,19 +23,46 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Prop, Provide, Watch } from 'vue-property-decorator';
+  import { defineComponent } from 'vue';
+
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
   import { defer } from 'utils/hueUtils';
 
-  @Component
-  export default class SidebarDrawer extends Vue {
-    @Prop()
-    show!: boolean;
+  export default defineComponent({
+    provide(): {
+      hideDrawer: () => void;
+    } {
+      return {
+        hideDrawer: (): void => {
+          this.$emit('close');
+        }
+      };
+    },
+
+    props: {
+      show: Boolean
+    },
+
+    emits: ['close'],
+
+    data(): {
+      subTracker: SubscriptionTracker;
+      deferredShow: boolean;
+    } {
+      return {
+        subTracker: new SubscriptionTracker(),
+        deferredShow: false
+      };
+    },
 
-    subTracker = new SubscriptionTracker();
-    deferredShow = false;
+    watch: {
+      show(newValue: boolean): void {
+        // deferredShow is used to prevent closing it immediately after the document click event triggered by opening
+        defer(() => {
+          this.deferredShow = newValue;
+        });
+      }
+    },
 
     mounted(): void {
       this.subTracker.addEventListener(document, 'click', (event: MouseEvent) => {
@@ -48,18 +75,5 @@
         }
       });
     }
-
-    @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>

+ 2 - 4
desktop/core/src/desktop/js/components/sidebar/SpacerItem.vue

@@ -21,9 +21,7 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
+  import { defineComponent } from 'vue';
 
-  @Component
-  export default class SpacerItem extends Vue {}
+  export default defineComponent({});
 </script>

+ 38 - 22
desktop/core/src/desktop/js/components/sidebar/UserDrawerContent.vue

@@ -26,8 +26,12 @@
       </div>
     </div>
     <ul>
-      <li v-for="childItem in children" :key="childItem.name">
-        <BaseNavigationItem :item="childItem" @click="hideDrawer">
+      <li v-for="(childItem, index) in children" :key="index">
+        <BaseNavigationItem
+          v-if="childItem.type === 'navigation'"
+          :item="childItem"
+          @click="hideDrawer"
+        >
           {{ childItem.displayName }}
         </BaseNavigationItem>
       </li>
@@ -41,29 +45,41 @@
 </template>
 
 <script lang="ts">
-  import Vue from 'vue';
-  import Component from 'vue-class-component';
-  import { Inject, Prop } from 'vue-property-decorator';
+  import { defineComponent, PropType } from 'vue';
+
   import BaseNavigationItem from './BaseNavigationItem.vue';
-  import { SidebarAccordionSubItem, SidebarNavigationItem, UserDrawerItem } from './types';
+  import { SidebarAccordionSubItem, SidebarBaseItem, UserDrawerItem } from './types';
+
+  export default defineComponent({
+    components: { BaseNavigationItem },
+
+    inject: ['hideDrawer'],
 
-  @Component({
-    components: { BaseNavigationItem }
-  })
-  export default class UserDrawerContent extends Vue {
-    @Inject()
-    hideDrawer!: () => void;
+    props: {
+      item: {
+        type: Object as PropType<UserDrawerItem>,
+        required: true
+      },
+      children: {
+        type: Object as PropType<SidebarAccordionSubItem[]>,
+        required: false,
+        default: []
+      }
+    },
 
-    @Prop()
-    item!: UserDrawerItem;
-    @Prop({ required: false, default: [] })
-    children?: SidebarAccordionSubItem[];
+    data(): {
+      hideDrawer?: () => void;
+    } {
+      return {};
+    },
 
-    get logoutItem(): Pick<SidebarNavigationItem, 'handler' | 'name'> {
-      return {
-        handler: this.item.logoutHandler,
-        name: 'logout'
-      };
+    computed: {
+      logoutItem(): SidebarBaseItem {
+        return {
+          handler: this.item.logoutHandler,
+          name: 'logout'
+        };
+      }
     }
-  }
+  });
 </script>

+ 10 - 8
desktop/core/src/desktop/js/components/sidebar/types.ts

@@ -14,28 +14,30 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export interface SidebarNavigationItem {
-  type: 'navigation';
+export interface SidebarBaseItem {
   name: string;
-  displayName: string;
   url?: string;
   handler?: (event: Event) => void;
+}
+
+export interface SidebarNavigationItem extends SidebarBaseItem {
+  type: 'navigation';
+  displayName: string;
   iconHtml: string;
 }
 
-export type SidebarAccordionSubItem = SidebarSpacerItem | Omit<SidebarNavigationItem, 'iconHtml'>;
+export type SidebarNavigationSubItem = Omit<SidebarNavigationItem, 'iconHtml'>;
+
+export type SidebarAccordionSubItem = SidebarSpacerItem | SidebarNavigationSubItem;
 
 export interface SidebarSpacerItem {
   type: 'spacer';
 }
 
-export interface SidebarAccordionItem {
+export interface SidebarAccordionItem extends SidebarBaseItem {
   type: 'accordion';
-  name: string;
   iconHtml: string;
   displayName: string;
-  url?: string;
-  handler?: (event: Event) => void;
   children: SidebarAccordionSubItem[];
 }