{
+ itemClicked = (event, child) => {
+ if (child.url === '/shortcuts') {
+ event.preventDefault();
+ appEvents.emit('show-modal', {
+ templateHtml: '',
+ });
+ }
+ };
+
+ switchOrg = () => {
+ appEvents.emit('show-modal', {
+ templateHtml: '',
+ });
+ };
+
+ render() {
+ const { link, user } = this.props;
+ return (
+
+ );
+ }
+}
+
+export default BottomNavLinks;
diff --git a/public/app/core/components/sidemenu/BottomSection.test.tsx b/public/app/core/components/sidemenu/BottomSection.test.tsx
new file mode 100644
index 00000000000..40307895c57
--- /dev/null
+++ b/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();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/sidemenu/BottomSection.tsx b/public/app/core/components/sidemenu/BottomSection.tsx
new file mode 100644
index 00000000000..ee5175ca763
--- /dev/null
+++ b/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 (
+
+ {!isSignedIn && }
+ {bottomNav.map((link, index) => {
+ return ;
+ })}
+
+ );
+}
diff --git a/public/app/core/components/sidemenu/DropDownChild.test.tsx b/public/app/core/components/sidemenu/DropDownChild.test.tsx
new file mode 100644
index 00000000000..6a15ca5b23f
--- /dev/null
+++ b/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();
+};
+
+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();
+ });
+});
diff --git a/public/app/core/components/sidemenu/DropDownChild.tsx b/public/app/core/components/sidemenu/DropDownChild.tsx
new file mode 100644
index 00000000000..1a577d185e5
--- /dev/null
+++ b/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 => {
+ const { child } = props;
+ const listItemClassName = child.divider ? 'divider' : '';
+
+ return (
+
+
+ {child.icon && }
+ {child.text}
+
+
+ );
+};
+
+export default DropDownChild;
diff --git a/public/app/core/components/sidemenu/SideMenu.test.tsx b/public/app/core/components/sidemenu/SideMenu.test.tsx
new file mode 100644
index 00000000000..2a262adca5a
--- /dev/null
+++ b/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();
+};
+
+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');
+ });
+ });
+});
diff --git a/public/app/core/components/sidemenu/SideMenu.tsx b/public/app/core/components/sidemenu/SideMenu.tsx
new file mode 100644
index 00000000000..0092c1e3842
--- /dev/null
+++ b/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 [
+
+

+
,
+
+
+
+ Close
+
+
,
+ ,
+ ,
+ ];
+ }
+}
diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.test.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.test.tsx
new file mode 100644
index 00000000000..c98d78ca021
--- /dev/null
+++ b/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();
+};
+
+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();
+ });
+});
diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx
new file mode 100644
index 00000000000..7cd7554f82c
--- /dev/null
+++ b/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 => {
+ const { link } = props;
+ return (
+
+ -
+ {link.text}
+
+ {link.children &&
+ link.children.map((child, index) => {
+ return ;
+ })}
+
+ );
+};
+
+export default SideMenuDropDown;
diff --git a/public/app/core/components/sidemenu/SignIn.test.tsx b/public/app/core/components/sidemenu/SignIn.test.tsx
new file mode 100644
index 00000000000..c202926086f
--- /dev/null
+++ b/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();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/sidemenu/SignIn.tsx b/public/app/core/components/sidemenu/SignIn.tsx
new file mode 100644
index 00000000000..17dd913823a
--- /dev/null
+++ b/public/app/core/components/sidemenu/SignIn.tsx
@@ -0,0 +1,23 @@
+import React, { SFC } from 'react';
+
+const SignIn: SFC = () => {
+ const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
+ return (
+
+ );
+};
+
+export default SignIn;
diff --git a/public/app/core/components/sidemenu/TopSection.test.tsx b/public/app/core/components/sidemenu/TopSection.test.tsx
new file mode 100644
index 00000000000..6ed01479833
--- /dev/null
+++ b/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();
+};
+
+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();
+ });
+});
diff --git a/public/app/core/components/sidemenu/TopSection.tsx b/public/app/core/components/sidemenu/TopSection.tsx
new file mode 100644
index 00000000000..c6bf5df8242
--- /dev/null
+++ b/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 = () => {
+ const navTree = _.cloneDeep(config.bootData.navTree);
+ const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
+
+ return (
+
+ {mainLinks.map((link, index) => {
+ return ;
+ })}
+
+ );
+};
+
+export default TopSection;
diff --git a/public/app/core/components/sidemenu/TopSectionItem.test.tsx b/public/app/core/components/sidemenu/TopSectionItem.test.tsx
new file mode 100644
index 00000000000..e7d3c4b7c75
--- /dev/null
+++ b/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();
+};
+
+describe('Render', () => {
+ it('should render component', () => {
+ const wrapper = setup();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/public/app/core/components/sidemenu/TopSectionItem.tsx b/public/app/core/components/sidemenu/TopSectionItem.tsx
new file mode 100644
index 00000000000..4a207cc0df9
--- /dev/null
+++ b/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 => {
+ const { link } = props;
+ return (
+
+ );
+};
+
+export default TopSectionItem;
diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap
new file mode 100644
index 00000000000..f3181b617ad
--- /dev/null
+++ b/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`] = `
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+`;
+
+exports[`Render should render component 1`] = `
+
+`;
+
+exports[`Render should render organisation switcher 1`] = `
+
+`;
+
+exports[`Render should render subtitle 1`] = `
+
+
+
+
+
+ -
+
+ subtitle
+
+
+ -
+
+
+
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomSection.test.tsx.snap
new file mode 100644
index 00000000000..ea92e8b7343
--- /dev/null
+++ b/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`] = `
+
+
+
+
+
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/DropDownChild.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/DropDownChild.test.tsx.snap
new file mode 100644
index 00000000000..fcfd8ea936a
--- /dev/null
+++ b/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`] = `
+
+
+
+`;
+
+exports[`Render should render icon if exists 1`] = `
+
+
+
+
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap
new file mode 100644
index 00000000000..78e64209749
--- /dev/null
+++ b/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 [
+ ,
+ ,
+ ,
+ ,
+]
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenuDropDown.test.tsx.snap
new file mode 100644
index 00000000000..861168c1cc3
--- /dev/null
+++ b/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`] = `
+
+`;
+
+exports[`Render should render component 1`] = `
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SignIn.test.tsx.snap
new file mode 100644
index 00000000000..ba21be63b51
--- /dev/null
+++ b/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`] = `
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/TopSection.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/TopSection.test.tsx.snap
new file mode 100644
index 00000000000..c78ec726302
--- /dev/null
+++ b/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`] = `
+
+
+
+`;
+
+exports[`Render should render items 1`] = `
+
+
+
+`;
diff --git a/public/app/core/components/sidemenu/__snapshots__/TopSectionItem.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/TopSectionItem.test.tsx.snap
new file mode 100644
index 00000000000..f7ff56bff6b
--- /dev/null
+++ b/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`] = `
+
+`;
diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html
deleted file mode 100644
index 3a4ce11333e..00000000000
--- a/public/app/core/components/sidemenu/sidemenu.html
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts
deleted file mode 100644
index f2798f874bd..00000000000
--- a/public/app/core/components/sidemenu/sidemenu.ts
+++ /dev/null
@@ -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: '',
- });
- }
-
- itemClicked(item, evt) {
- if (item.url === '/shortcuts') {
- appEvents.emit('show-modal', {
- templateHtml: '',
- });
- 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);
diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index bff98e1fbb3..173d6b80b15 100644
--- a/public/app/core/core.ts
+++ b/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,
diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts
index c2742c82958..c40fbff2ab2 100644
--- a/public/app/core/services/context_srv.ts
+++ b/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;