Pārlūkot izejas kodu

Reactify sidebar (#13091)

* created react component and moved markdown

* extracting components

* Broke out parts into components

* tests

* Flattened file structure

* Tests

* made instances typed in test

* typing

* function instead of variable

* updated user model with missing properties

* added full set of properties to user mock

* redone from variable to function

* refactor: minor refactorings of #13091

* removed logging
Peter Holmberg 7 gadi atpakaļ
vecāks
revīzija
cab6861d27
29 mainītis faili ar 995 papildinājumiem un 172 dzēšanām
  1. 2 0
      public/app/core/angular_wrappers.ts
  2. 96 0
      public/app/core/components/sidemenu/BottomNavLinks.test.tsx
  3. 78 0
      public/app/core/components/sidemenu/BottomNavLinks.tsx
  4. 44 0
      public/app/core/components/sidemenu/BottomSection.test.tsx
  5. 29 0
      public/app/core/components/sidemenu/BottomSection.tsx
  6. 35 0
      public/app/core/components/sidemenu/DropDownChild.test.tsx
  7. 21 0
      public/app/core/components/sidemenu/DropDownChild.tsx
  8. 70 0
      public/app/core/components/sidemenu/SideMenu.test.tsx
  9. 32 0
      public/app/core/components/sidemenu/SideMenu.tsx
  10. 35 0
      public/app/core/components/sidemenu/SideMenuDropDown.test.tsx
  11. 23 0
      public/app/core/components/sidemenu/SideMenuDropDown.tsx
  12. 11 0
      public/app/core/components/sidemenu/SignIn.test.tsx
  13. 23 0
      public/app/core/components/sidemenu/SignIn.tsx
  14. 41 0
      public/app/core/components/sidemenu/TopSection.test.tsx
  15. 19 0
      public/app/core/components/sidemenu/TopSection.tsx
  16. 22 0
      public/app/core/components/sidemenu/TopSectionItem.test.tsx
  17. 23 0
      public/app/core/components/sidemenu/TopSectionItem.tsx
  18. 163 0
      public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap
  19. 34 0
      public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap
  20. 21 0
      public/app/core/components/sidemenu/__snapshots__/DropDownChild.test.tsx.snap
  21. 22 0
      public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap
  22. 59 0
      public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap
  23. 40 0
      public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap
  24. 33 0
      public/app/core/components/sidemenu/__snapshots__/TopSection.test.tsx.snap
  25. 17 0
      public/app/core/components/sidemenu/__snapshots__/TopSectionItem.test.tsx.snap
  26. 0 81
      public/app/core/components/sidemenu/sidemenu.html
  27. 0 89
      public/app/core/components/sidemenu/sidemenu.ts
  28. 0 2
      public/app/core/core.ts
  29. 2 0
      public/app/core/services/context_srv.ts

+ 2 - 0
public/app/core/angular_wrappers.ts

@@ -4,10 +4,12 @@ import PageHeader from './components/PageHeader/PageHeader';
 import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import { SearchResult } from './components/search/SearchResult';
 import { TagFilter } from './components/TagFilter/TagFilter';
+import { SideMenu } from './components/sidemenu/SideMenu';
 import DashboardPermissions from './components/Permissions/DashboardPermissions';
 
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
+  react2AngularDirective('sidemenu', SideMenu, []);
   react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
   react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
   react2AngularDirective('searchResult', SearchResult, []);

+ 96 - 0
public/app/core/components/sidemenu/BottomNavLinks.test.tsx

@@ -0,0 +1,96 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import BottomNavLinks from './BottomNavLinks';
+import appEvents from '../../app_events';
+
+jest.mock('../../app_events', () => ({
+  emit: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      link: {},
+      user: {
+        isGrafanaAdmin: false,
+        isSignedIn: false,
+        orgCount: 2,
+        orgRole: '',
+        orgId: 1,
+        orgName: 'Grafana',
+        timezone: 'UTC',
+        helpFlags1: 1,
+        lightTheme: false,
+        hasEditPermissionInFolders: false,
+      },
+    },
+    propOverrides
+  );
+  return shallow(<BottomNavLinks {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render organisation switcher', () => {
+    const wrapper = setup({
+      link: {
+        showOrgSwitcher: true,
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render subtitle', () => {
+    const wrapper = setup({
+      link: {
+        subTitle: 'subtitle',
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render children', () => {
+    const wrapper = setup({
+      link: {
+        children: [
+          {
+            id: '1',
+          },
+          {
+            id: '2',
+          },
+          {
+            id: '3',
+          },
+          {
+            id: '4',
+            hideFromMenu: true,
+          },
+        ],
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  describe('item clicked', () => {
+    const wrapper = setup();
+    const mockEvent = { preventDefault: jest.fn() };
+    it('should emit show modal event if url matches shortcut', () => {
+      const child = { url: '/shortcuts' };
+      const instance = wrapper.instance() as BottomNavLinks;
+      instance.itemClicked(mockEvent, child);
+
+      expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { templateHtml: '<help-modal></help-modal>' });
+    });
+  });
+});

+ 78 - 0
public/app/core/components/sidemenu/BottomNavLinks.tsx

@@ -0,0 +1,78 @@
+import React, { PureComponent } from 'react';
+import appEvents from '../../app_events';
+import { User } from '../../services/context_srv';
+
+export interface Props {
+  link: any;
+  user: User;
+}
+
+class BottomNavLinks extends PureComponent<Props> {
+  itemClicked = (event, child) => {
+    if (child.url === '/shortcuts') {
+      event.preventDefault();
+      appEvents.emit('show-modal', {
+        templateHtml: '<help-modal></help-modal>',
+      });
+    }
+  };
+
+  switchOrg = () => {
+    appEvents.emit('show-modal', {
+      templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
+    });
+  };
+
+  render() {
+    const { link, user } = this.props;
+    return (
+      <div className="sidemenu-item dropdown dropup">
+        <a href={link.url} className="sidemenu-link" target={link.target}>
+          <span className="icon-circle sidemenu-icon">
+            {link.icon && <i className={link.icon} />}
+            {link.img && <img src={link.img} />}
+          </span>
+        </a>
+        <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
+          {link.subTitle && (
+            <li className="sidemenu-subtitle">
+              <span className="sidemenu-item-text">{link.subTitle}</span>
+            </li>
+          )}
+          {link.showOrgSwitcher && (
+            <li className="sidemenu-org-switcher">
+              <a onClick={this.switchOrg}>
+                <div>
+                  <div className="sidemenu-org-switcher__org-name">{user.orgName}</div>
+                  <div className="sidemenu-org-switcher__org-current">Current Org:</div>
+                </div>
+                <div className="sidemenu-org-switcher__switch">
+                  <i className="fa fa-fw fa-random" />Switch
+                </div>
+              </a>
+            </li>
+          )}
+          {link.children &&
+            link.children.map((child, index) => {
+              if (!child.hideFromMenu) {
+                return (
+                  <li className={child.divider} key={`${child.text}-${index}`}>
+                    <a href={child.url} target={child.target} onClick={event => this.itemClicked(event, child)}>
+                      {child.icon && <i className={child.icon} />}
+                      {child.text}
+                    </a>
+                  </li>
+                );
+              }
+              return null;
+            })}
+          <li className="side-menu-header">
+            <span className="sidemenu-item-text">{link.text}</span>
+          </li>
+        </ul>
+      </div>
+    );
+  }
+}
+
+export default BottomNavLinks;

+ 44 - 0
public/app/core/components/sidemenu/BottomSection.test.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import BottomSection from './BottomSection';
+
+jest.mock('../../config', () => ({
+  bootData: {
+    navTree: [
+      {
+        id: 'profile',
+        hideFromMenu: true,
+      },
+      {
+        hideFromMenu: true,
+      },
+      {
+        hideFromMenu: false,
+      },
+      {
+        hideFromMenu: true,
+      },
+    ],
+  },
+  user: {
+    orgCount: 5,
+    orgName: 'Grafana',
+  },
+}));
+
+jest.mock('app/core/services/context_srv', () => ({
+  contextSrv: {
+    sidemenu: true,
+    isSignedIn: false,
+    isGrafanaAdmin: false,
+    hasEditPermissionFolders: false,
+  },
+}));
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = shallow(<BottomSection />);
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 29 - 0
public/app/core/components/sidemenu/BottomSection.tsx

@@ -0,0 +1,29 @@
+import React from 'react';
+import _ from 'lodash';
+import SignIn from './SignIn';
+import BottomNavLinks from './BottomNavLinks';
+import { contextSrv } from 'app/core/services/context_srv';
+import config from '../../config';
+
+export default function BottomSection() {
+  const navTree = _.cloneDeep(config.bootData.navTree);
+  const bottomNav = _.filter(navTree, item => item.hideFromMenu);
+  const isSignedIn = contextSrv.isSignedIn;
+  const user = contextSrv.user;
+
+  if (user && user.orgCount > 1) {
+    const profileNode = _.find(bottomNav, { id: 'profile' });
+    if (profileNode) {
+      profileNode.showOrgSwitcher = true;
+    }
+  }
+
+  return (
+    <div className="sidemenu__bottom">
+      {!isSignedIn && <SignIn />}
+      {bottomNav.map((link, index) => {
+        return <BottomNavLinks link={link} user={user} key={`${link.url}-${index}`} />;
+      })}
+    </div>
+  );
+}

+ 35 - 0
public/app/core/components/sidemenu/DropDownChild.test.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import DropDownChild from './DropDownChild';
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      child: {
+        divider: true,
+      },
+    },
+    propOverrides
+  );
+
+  return shallow(<DropDownChild {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render icon if exists', () => {
+    const wrapper = setup({
+      child: {
+        divider: false,
+        icon: 'icon-test',
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 21 - 0
public/app/core/components/sidemenu/DropDownChild.tsx

@@ -0,0 +1,21 @@
+import React, { SFC } from 'react';
+
+export interface Props {
+  child: any;
+}
+
+const DropDownChild: SFC<Props> = props => {
+  const { child } = props;
+  const listItemClassName = child.divider ? 'divider' : '';
+
+  return (
+    <li className={listItemClassName}>
+      <a href={child.url}>
+        {child.icon && <i className={child.icon} />}
+        {child.text}
+      </a>
+    </li>
+  );
+};
+
+export default DropDownChild;

+ 70 - 0
public/app/core/components/sidemenu/SideMenu.test.tsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { SideMenu } from './SideMenu';
+import appEvents from '../../app_events';
+import { contextSrv } from 'app/core/services/context_srv';
+
+jest.mock('../../app_events', () => ({
+  emit: jest.fn(),
+}));
+
+jest.mock('app/core/services/context_srv', () => ({
+  contextSrv: {
+    sidemenu: true,
+    user: {},
+    isSignedIn: false,
+    isGrafanaAdmin: false,
+    isEditor: false,
+    hasEditPermissionFolders: false,
+    toggleSideMenu: jest.fn(),
+  },
+}));
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      loginUrl: '',
+      user: {},
+      mainLinks: [],
+      bottomeLinks: [],
+      isSignedIn: false,
+    },
+    propOverrides
+  );
+
+  return shallow(<SideMenu {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  describe('toggle side menu', () => {
+    const wrapper = setup();
+    const instance = wrapper.instance() as SideMenu;
+    instance.toggleSideMenu();
+
+    it('should call contextSrv.toggleSideMenu', () => {
+      expect(contextSrv.toggleSideMenu).toHaveBeenCalled();
+    });
+
+    it('should emit toggle sidemenu event', () => {
+      expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu');
+    });
+  });
+
+  describe('toggle side menu on mobile', () => {
+    const wrapper = setup();
+    const instance = wrapper.instance() as SideMenu;
+    instance.toggleSideMenuSmallBreakpoint();
+
+    it('should emit toggle sidemenu event', () => {
+      expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu-mobile');
+    });
+  });
+});

+ 32 - 0
public/app/core/components/sidemenu/SideMenu.tsx

@@ -0,0 +1,32 @@
+import React, { PureComponent } from 'react';
+import appEvents from '../../app_events';
+import { contextSrv } from 'app/core/services/context_srv';
+import TopSection from './TopSection';
+import BottomSection from './BottomSection';
+
+export class SideMenu extends PureComponent {
+  toggleSideMenu = () => {
+    contextSrv.toggleSideMenu();
+    appEvents.emit('toggle-sidemenu');
+  };
+
+  toggleSideMenuSmallBreakpoint = () => {
+    appEvents.emit('toggle-sidemenu-mobile');
+  };
+
+  render() {
+    return [
+      <div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
+        <img src="public/img/grafana_icon.svg" alt="graphana_logo" />
+      </div>,
+      <div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
+        <i className="fa fa-bars" />
+        <span className="sidemenu__close">
+          <i className="fa fa-times" />&nbsp;Close
+        </span>
+      </div>,
+      <TopSection key="topsection" />,
+      <BottomSection key="bottomsection" />,
+    ];
+  }
+}

+ 35 - 0
public/app/core/components/sidemenu/SideMenuDropDown.test.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import SideMenuDropDown from './SideMenuDropDown';
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      link: {
+        text: 'link',
+      },
+    },
+    propOverrides
+  );
+
+  return shallow(<SideMenuDropDown {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render children', () => {
+    const wrapper = setup({
+      link: {
+        text: 'link',
+        children: [{ id: 1 }, { id: 2 }, { id: 3 }],
+      },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 23 - 0
public/app/core/components/sidemenu/SideMenuDropDown.tsx

@@ -0,0 +1,23 @@
+import React, { SFC } from 'react';
+import DropDownChild from './DropDownChild';
+
+interface Props {
+  link: any;
+}
+
+const SideMenuDropDown: SFC<Props> = props => {
+  const { link } = props;
+  return (
+    <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
+      <li className="side-menu-header">
+        <span className="sidemenu-item-text">{link.text}</span>
+      </li>
+      {link.children &&
+        link.children.map((child, index) => {
+          return <DropDownChild child={child} key={`${child.url}-${index}`} />;
+        })}
+    </ul>
+  );
+};
+
+export default SideMenuDropDown;

+ 11 - 0
public/app/core/components/sidemenu/SignIn.test.tsx

@@ -0,0 +1,11 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import SignIn from './SignIn';
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = shallow(<SignIn />);
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 23 - 0
public/app/core/components/sidemenu/SignIn.tsx

@@ -0,0 +1,23 @@
+import React, { SFC } from 'react';
+
+const SignIn: SFC<any> = () => {
+  const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
+  return (
+    <div className="sidemenu-item">
+      <a href={loginUrl} className="sidemenu-link" target="_self">
+        <span className="icon-circle sidemenu-icon">
+          <i className="fa fa-fw fa-sign-in" />
+        </span>
+      </a>
+      <a href={loginUrl} target="_self">
+        <ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
+          <li className="side-menu-header">
+            <span className="sidemenu-item-text">Sign In</span>
+          </li>
+        </ul>
+      </a>
+    </div>
+  );
+};
+
+export default SignIn;

+ 41 - 0
public/app/core/components/sidemenu/TopSection.test.tsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import TopSection from './TopSection';
+
+jest.mock('../../config', () => ({
+  bootData: {
+    navTree: [
+      { id: '1', hideFromMenu: true },
+      { id: '2', hideFromMenu: true },
+      { id: '3', hideFromMenu: false },
+      { id: '4', hideFromMenu: true },
+    ],
+  },
+}));
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      mainLinks: [],
+    },
+    propOverrides
+  );
+
+  return shallow(<TopSection {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render items', () => {
+    const wrapper = setup({
+      mainLinks: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 19 - 0
public/app/core/components/sidemenu/TopSection.tsx

@@ -0,0 +1,19 @@
+import React, { SFC } from 'react';
+import _ from 'lodash';
+import TopSectionItem from './TopSectionItem';
+import config from '../../config';
+
+const TopSection: SFC<any> = () => {
+  const navTree = _.cloneDeep(config.bootData.navTree);
+  const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
+
+  return (
+    <div className="sidemenu__top">
+      {mainLinks.map((link, index) => {
+        return <TopSectionItem link={link} key={`${link.id}-${index}`} />;
+      })}
+    </div>
+  );
+};
+
+export default TopSection;

+ 22 - 0
public/app/core/components/sidemenu/TopSectionItem.test.tsx

@@ -0,0 +1,22 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import TopSectionItem from './TopSectionItem';
+
+const setup = (propOverrides?: object) => {
+  const props = Object.assign(
+    {
+      link: {},
+    },
+    propOverrides
+  );
+
+  return shallow(<TopSectionItem {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 23 - 0
public/app/core/components/sidemenu/TopSectionItem.tsx

@@ -0,0 +1,23 @@
+import React, { SFC } from 'react';
+import SideMenuDropDown from './SideMenuDropDown';
+
+export interface Props {
+  link: any;
+}
+
+const TopSectionItem: SFC<Props> = props => {
+  const { link } = props;
+  return (
+    <div className="sidemenu-item dropdown">
+      <a className="sidemenu-link" href={link.url} target={link.target}>
+        <span className="icon-circle sidemenu-icon">
+          <i className={link.icon} />
+          {link.img && <img src={link.img} />}
+        </span>
+      </a>
+      {link.children && <SideMenuDropDown link={link} />}
+    </div>
+  );
+};
+
+export default TopSectionItem;

+ 163 - 0
public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap

@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render children 1`] = `
+<div
+  className="sidemenu-item dropdown dropup"
+>
+  <a
+    className="sidemenu-link"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu--sidemenu"
+    role="menu"
+  >
+    <li
+      key="undefined-0"
+    >
+      <a
+        onClick={[Function]}
+      />
+    </li>
+    <li
+      key="undefined-1"
+    >
+      <a
+        onClick={[Function]}
+      />
+    </li>
+    <li
+      key="undefined-2"
+    >
+      <a
+        onClick={[Function]}
+      />
+    </li>
+    <li
+      className="side-menu-header"
+    >
+      <span
+        className="sidemenu-item-text"
+      />
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`Render should render component 1`] = `
+<div
+  className="sidemenu-item dropdown dropup"
+>
+  <a
+    className="sidemenu-link"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu--sidemenu"
+    role="menu"
+  >
+    <li
+      className="side-menu-header"
+    >
+      <span
+        className="sidemenu-item-text"
+      />
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`Render should render organisation switcher 1`] = `
+<div
+  className="sidemenu-item dropdown dropup"
+>
+  <a
+    className="sidemenu-link"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu--sidemenu"
+    role="menu"
+  >
+    <li
+      className="sidemenu-org-switcher"
+    >
+      <a
+        onClick={[Function]}
+      >
+        <div>
+          <div
+            className="sidemenu-org-switcher__org-name"
+          >
+            Grafana
+          </div>
+          <div
+            className="sidemenu-org-switcher__org-current"
+          >
+            Current Org:
+          </div>
+        </div>
+        <div
+          className="sidemenu-org-switcher__switch"
+        >
+          <i
+            className="fa fa-fw fa-random"
+          />
+          Switch
+        </div>
+      </a>
+    </li>
+    <li
+      className="side-menu-header"
+    >
+      <span
+        className="sidemenu-item-text"
+      />
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`Render should render subtitle 1`] = `
+<div
+  className="sidemenu-item dropdown dropup"
+>
+  <a
+    className="sidemenu-link"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu--sidemenu"
+    role="menu"
+  >
+    <li
+      className="sidemenu-subtitle"
+    >
+      <span
+        className="sidemenu-item-text"
+      >
+        subtitle
+      </span>
+    </li>
+    <li
+      className="side-menu-header"
+    >
+      <span
+        className="sidemenu-item-text"
+      />
+    </li>
+  </ul>
+</div>
+`;

+ 34 - 0
public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap

@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="sidemenu__bottom"
+>
+  <SignIn />
+  <BottomNavLinks
+    key="undefined-0"
+    link={
+      Object {
+        "hideFromMenu": true,
+        "id": "profile",
+      }
+    }
+  />
+  <BottomNavLinks
+    key="undefined-1"
+    link={
+      Object {
+        "hideFromMenu": true,
+      }
+    }
+  />
+  <BottomNavLinks
+    key="undefined-2"
+    link={
+      Object {
+        "hideFromMenu": true,
+      }
+    }
+  />
+</div>
+`;

+ 21 - 0
public/app/core/components/sidemenu/__snapshots__/DropDownChild.test.tsx.snap

@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<li
+  className="divider"
+>
+  <a />
+</li>
+`;
+
+exports[`Render should render icon if exists 1`] = `
+<li
+  className=""
+>
+  <a>
+    <i
+      className="icon-test"
+    />
+  </a>
+</li>
+`;

+ 22 - 0
public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap

@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+Array [
+  <div
+    className="sidemenu__logo"
+    key="logo"
+    onClick={[Function]}
+  />,
+  <div
+    className="sidemenu__logo_small_breakpoint"
+    key="hamburger"
+    onClick={[Function]}
+  />,
+  <TopSection
+    key="topsection"
+  />,
+  <BottomSection
+    key="bottomsection"
+  />,
+]
+`;

+ 59 - 0
public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap

@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render children 1`] = `
+<ul
+  className="dropdown-menu dropdown-menu--sidemenu"
+  role="menu"
+>
+  <li
+    className="side-menu-header"
+  >
+    <span
+      className="sidemenu-item-text"
+    >
+      link
+    </span>
+  </li>
+  <DropDownChild
+    child={
+      Object {
+        "id": 1,
+      }
+    }
+    key="undefined-0"
+  />
+  <DropDownChild
+    child={
+      Object {
+        "id": 2,
+      }
+    }
+    key="undefined-1"
+  />
+  <DropDownChild
+    child={
+      Object {
+        "id": 3,
+      }
+    }
+    key="undefined-2"
+  />
+</ul>
+`;
+
+exports[`Render should render component 1`] = `
+<ul
+  className="dropdown-menu dropdown-menu--sidemenu"
+  role="menu"
+>
+  <li
+    className="side-menu-header"
+  >
+    <span
+      className="sidemenu-item-text"
+    >
+      link
+    </span>
+  </li>
+</ul>
+`;

+ 40 - 0
public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap

@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="sidemenu-item"
+>
+  <a
+    className="sidemenu-link"
+    href="login?redirect=blank"
+    target="_self"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    >
+      <i
+        className="fa fa-fw fa-sign-in"
+      />
+    </span>
+  </a>
+  <a
+    href="login?redirect=blank"
+    target="_self"
+  >
+    <ul
+      className="dropdown-menu dropdown-menu--sidemenu"
+      role="menu"
+    >
+      <li
+        className="side-menu-header"
+      >
+        <span
+          className="sidemenu-item-text"
+        >
+          Sign In
+        </span>
+      </li>
+    </ul>
+  </a>
+</div>
+`;

+ 33 - 0
public/app/core/components/sidemenu/__snapshots__/TopSection.test.tsx.snap

@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="sidemenu__top"
+>
+  <TopSectionItem
+    key="3-0"
+    link={
+      Object {
+        "hideFromMenu": false,
+        "id": "3",
+      }
+    }
+  />
+</div>
+`;
+
+exports[`Render should render items 1`] = `
+<div
+  className="sidemenu__top"
+>
+  <TopSectionItem
+    key="3-0"
+    link={
+      Object {
+        "hideFromMenu": false,
+        "id": "3",
+      }
+    }
+  />
+</div>
+`;

+ 17 - 0
public/app/core/components/sidemenu/__snapshots__/TopSectionItem.test.tsx.snap

@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="sidemenu-item dropdown"
+>
+  <a
+    className="sidemenu-link"
+  >
+    <span
+      className="icon-circle sidemenu-icon"
+    >
+      <i />
+    </span>
+  </a>
+</div>
+`;

+ 0 - 81
public/app/core/components/sidemenu/sidemenu.html

@@ -1,81 +0,0 @@
-<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
-  <img src="public/img/grafana_icon.svg"></img>
-</a>
-
-<a class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
-  <i class="fa fa-bars"></i>
-  <span class="sidemenu__close">
-    <i class="fa fa-times"></i>&nbsp;Close</span>
-</a>
-
-<div class="sidemenu__top">
-  <div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
-    <a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
-      <span class="icon-circle sidemenu-icon">
-        <i class="{{::item.icon}}" ng-show="::item.icon"></i>
-        <img ng-src="{{::item.img}}" ng-show="::item.img">
-      </span>
-    </a>
-    <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
-      <li class="side-menu-header">
-        <span class="sidemenu-item-text">{{::item.text}}</span>
-      </li>
-      <li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
-        <a href="{{::child.url}}">
-          <i class="{{::child.icon}}" ng-show="::child.icon"></i>
-          {{::child.text}}
-        </a>
-      </li>
-    </ul>
-  </div>
-</div>
-
-<div class="sidemenu__bottom">
-  <div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
-    <a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
-      <span class="icon-circle sidemenu-icon">
-        <i class="fa fa-fw fa-sign-in"></i>
-      </span>
-    </a>
-    <a href="{{ctrl.loginUrl}}" target="_self">
-      <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
-        <li class="side-menu-header">
-          <span class="sidemenu-item-text">Sign In</span>
-        </li>
-      </ul>
-    </a>
-  </div>
-
-  <div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
-    <a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
-      <span class="icon-circle sidemenu-icon">
-        <i class="{{::item.icon}}" ng-show="::item.icon"></i>
-        <img ng-src="{{::item.img}}" ng-show="::item.img">
-      </span>
-    </a>
-    <ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
-      <li ng-if="item.subTitle" class="sidemenu-subtitle">
-        <span class="sidemenu-item-text">{{::item.subTitle}}</span>
-      </li>
-      <li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
-        <a ng-click="ctrl.switchOrg()">
-          <div>
-            <div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
-            <div class="sidemenu-org-switcher__org-current">Current Org:</div>
-          </div>
-          <div class="sidemenu-org-switcher__switch">
-            <i class="fa fa-fw fa-random"></i>Switch</div>
-        </a>
-      </li>
-      <li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
-        <a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
-          <i class="{{::child.icon}}" ng-show="::child.icon"></i>
-          {{::child.text}}
-        </a>
-      </li>
-      <li class="side-menu-header">
-        <span class="sidemenu-item-text">{{::item.text}}</span>
-      </li>
-    </ul>
-  </div>
-</div>

+ 0 - 89
public/app/core/components/sidemenu/sidemenu.ts

@@ -1,89 +0,0 @@
-import _ from 'lodash';
-import config from 'app/core/config';
-import $ from 'jquery';
-import coreModule from '../../core_module';
-import appEvents from 'app/core/app_events';
-
-export class SideMenuCtrl {
-  user: any;
-  mainLinks: any;
-  bottomNav: any;
-  loginUrl: string;
-  isSignedIn: boolean;
-  isOpenMobile: boolean;
-
-  /** @ngInject */
-  constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
-    this.isSignedIn = contextSrv.isSignedIn;
-    this.user = contextSrv.user;
-
-    const navTree = _.cloneDeep(config.bootData.navTree);
-    this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
-    this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
-    this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
-
-    if (contextSrv.user.orgCount > 1) {
-      const profileNode = _.find(this.bottomNav, { id: 'profile' });
-      if (profileNode) {
-        profileNode.showOrgSwitcher = true;
-      }
-    }
-
-    this.$scope.$on('$routeChangeSuccess', () => {
-      this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
-    });
-  }
-
-  toggleSideMenu() {
-    this.contextSrv.toggleSideMenu();
-    appEvents.emit('toggle-sidemenu');
-
-    this.$timeout(() => {
-      this.$rootScope.$broadcast('render');
-    });
-  }
-
-  toggleSideMenuSmallBreakpoint() {
-    appEvents.emit('toggle-sidemenu-mobile');
-  }
-
-  switchOrg() {
-    this.$rootScope.appEvent('show-modal', {
-      templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
-    });
-  }
-
-  itemClicked(item, evt) {
-    if (item.url === '/shortcuts') {
-      appEvents.emit('show-modal', {
-        templateHtml: '<help-modal></help-modal>',
-      });
-      evt.preventDefault();
-    }
-  }
-}
-
-export function sideMenuDirective() {
-  return {
-    restrict: 'E',
-    templateUrl: 'public/app/core/components/sidemenu/sidemenu.html',
-    controller: SideMenuCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    scope: {},
-    link: function(scope, elem) {
-      // hack to hide dropdown menu
-      elem.on('click.dropdown', '.dropdown-menu a', function(evt) {
-        const menu = $(evt.target).parents('.dropdown-menu');
-        const parent = menu.parent();
-        menu.detach();
-
-        setTimeout(function() {
-          parent.append(menu);
-        }, 100);
-      });
-    },
-  };
-}
-
-coreModule.directive('sidemenu', sideMenuDirective);

+ 0 - 2
public/app/core/core.ts

@@ -20,7 +20,6 @@ import './services/search_srv';
 import './services/ng_react';
 
 import { grafanaAppDirective } from './components/grafana_app';
-import { sideMenuDirective } from './components/sidemenu/sidemenu';
 import { searchDirective } from './components/search/search';
 import { infoPopover } from './components/info_popover';
 import { navbarDirective } from './components/navbar/navbar';
@@ -62,7 +61,6 @@ export {
   arrayJoin,
   coreModule,
   grafanaAppDirective,
-  sideMenuDirective,
   navbarDirective,
   searchDirective,
   liveSrv,

+ 2 - 0
public/app/core/services/context_srv.ts

@@ -8,6 +8,8 @@ export class User {
   isSignedIn: any;
   orgRole: any;
   orgId: number;
+  orgName: string;
+  orgCount: number;
   timezone: string;
   helpFlags1: number;
   lightTheme: boolean;