Added possiblity to extend the dashboard programmatically

Change-Id: Ibf2c2f7e6d51e6f5a661021f3f9f4b15c9cbefa1
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index e6f740e..4b89ac8 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -18,6 +18,7 @@
 import {ModelSetup} from './services/helpers/model-setup.helpers';
 import {xosSidePanel} from './side-panel/side-panel';
 import {XosSidePanel} from './side-panel/side-panel.service';
+import {XosComponentInjector} from './services/helpers/component-injector.helpers';
 
 export const xosCore = 'xosCore';
 
@@ -35,6 +36,7 @@
   .service('ConfigHelpers', ConfigHelpers)
   .service('ModelSetup', ModelSetup)
   .service('XosSidePanel', XosSidePanel)
+  .service('XosComponentInjector', XosComponentInjector)
   .directive('xosLinkWrapper', xosLinkWrapper)
   .component('xosHeader', xosHeader)
   .component('xosFooter', xosFooter)
diff --git a/src/app/core/nav/nav.html b/src/app/core/nav/nav.html
index 402d2fa..ce495da 100644
--- a/src/app/core/nav/nav.html
+++ b/src/app/core/nav/nav.html
@@ -44,7 +44,8 @@
             <a ng-click="vm.logout()" class="btn btn-accent btn-block btn-logout">Logout</a>
           </div>
         </div>
-        <!--<a ng-click="vm.togglePanel()" class="btn btn-success">Open XSP</a>-->
+        <a ng-click="vm.togglePanel()" class="btn btn-success">Open XSP</a>
+        <a ng-click="vm.addToDashboard()" class="btn btn-success">Add to home</a>
       </li>
     </ul>
   </nav>
diff --git a/src/app/core/nav/nav.spec.ts b/src/app/core/nav/nav.spec.ts
index f443d91..5101ba8 100644
--- a/src/app/core/nav/nav.spec.ts
+++ b/src/app/core/nav/nav.spec.ts
@@ -32,7 +32,8 @@
       .service('NavigationService', NavigationService)
       .value('AuthService', AuthMock)
       .value('StyleConfig', {})
-      .value('XosSidePanel', {});
+      .value('XosSidePanel', {})
+      .value('XosComponentInjector', {});
     angular.mock.module('xosNav');
   });
 
diff --git a/src/app/core/nav/nav.ts b/src/app/core/nav/nav.ts
index ec2d8b2..08ad48a 100644
--- a/src/app/core/nav/nav.ts
+++ b/src/app/core/nav/nav.ts
@@ -3,9 +3,10 @@
 import {IXosAuthService} from '../../datasources/rest/auth.rest';
 import {IXosStyleConfig} from '../../../index';
 import {IXosSidePanelService} from '../side-panel/side-panel.service';
+import {IXosComponentInjectorService} from '../services/helpers/component-injector.helpers';
 
 class NavCtrl {
-  static $inject = ['$scope', '$state', 'NavigationService', 'AuthService', 'StyleConfig', 'XosSidePanel'];
+  static $inject = ['$scope', '$state', 'NavigationService', 'AuthService', 'StyleConfig', 'XosSidePanel', 'XosComponentInjector'];
   public routes: IXosNavigationRoute[];
   public navSelected: string;
   public appName: string;
@@ -17,7 +18,8 @@
     private navigationService: IXosNavigationService,
     private authService: IXosAuthService,
     private StyleConfig: IXosStyleConfig,
-    private XosSidePanel: IXosSidePanelService
+    private XosSidePanel: IXosSidePanelService,
+    private XosComponentInjector: IXosComponentInjectorService
   ) {
     // NOTE we'll need to have:
     // - Base routes (defined from configuration based on BRAND)
@@ -62,6 +64,9 @@
   togglePanel() {
     this.XosSidePanel.injectComponent('xosAlert', {config: {type: 'danger'}, show: true}, 'Sample message');
   }
+  addToDashboard() {
+    this.XosComponentInjector.injectComponent('#dashboard-component-container', 'xosAlert', {config: {type: 'danger'}, show: true}, 'Sample message', false);
+  }
 
   logout() {
     this.authService.logout()
diff --git a/src/app/core/services/helpers/component-injector.helpers.spec.ts b/src/app/core/services/helpers/component-injector.helpers.spec.ts
new file mode 100644
index 0000000..0ba501f
--- /dev/null
+++ b/src/app/core/services/helpers/component-injector.helpers.spec.ts
@@ -0,0 +1,52 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import 'angular-ui-router';
+import * as $ from 'jquery';
+import {XosComponentInjector, IXosComponentInjectorService} from './component-injector.helpers';
+
+let service: IXosComponentInjectorService;
+let element, scope: angular.IRootScopeService, compile: ng.ICompileService;
+
+describe('The XosComponentInjector service', () => {
+  beforeEach(() => {
+    angular
+      .module('test', [])
+      .component('extension', {
+        template: 'extended'
+      })
+      .component('target', {
+        template: `<div id="target"></div>`
+      })
+      .service('XosComponentInjector', XosComponentInjector);
+
+    angular.mock.module('test');
+  });
+
+  beforeEach(angular.mock.inject((
+    XosComponentInjector: IXosComponentInjectorService,
+  ) => {
+    service = XosComponentInjector;
+  }));
+
+  beforeEach(angular.mock.inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => {
+    scope = $rootScope;
+    compile = $compile;
+    element = $compile('<target></target>')($rootScope);
+    $rootScope.$digest();
+  }));
+
+  it('should have an InjectComponent method', () => {
+    expect(service.injectComponent).toBeDefined();
+  });
+
+  it('should have an removeInjectedComponents method', () => {
+    expect(service.removeInjectedComponents).toBeDefined();
+  });
+
+  it('should add a component to the target container', () => {
+    service.injectComponent($('#target', element), 'extension');
+    scope.$apply();
+    const extension = $('extension', element);
+    expect(extension.text()).toBe('extended');
+  });
+});
diff --git a/src/app/core/services/helpers/component-injector.helpers.ts b/src/app/core/services/helpers/component-injector.helpers.ts
new file mode 100644
index 0000000..8fd7a50
--- /dev/null
+++ b/src/app/core/services/helpers/component-injector.helpers.ts
@@ -0,0 +1,61 @@
+import * as $ from 'jquery';
+import * as _ from 'lodash';
+
+export interface IXosComponentInjectorService {
+  injectComponent(target: string | JQuery, componentName: string, attributes?: any, transclude?: string, clean?: boolean): void;
+  removeInjectedComponents(target: string | JQuery): void;
+}
+
+export class XosComponentInjector implements IXosComponentInjectorService {
+  static $inject = ['$rootScope', '$compile'];
+
+  constructor (
+    private $rootScope: ng.IRootScopeService,
+    private $compile: ng.ICompileService
+  ) {
+  }
+
+  public injectComponent(target: string | JQuery, componentName: string, attributes?: any, transclude?: string, clean?: boolean) {
+    let targetEl;
+    if (angular.isString(target)) {
+      targetEl = $(target);
+    }
+    else {
+      targetEl = target;
+    }
+
+    const componentTagName = this.camelToSnakeCase(componentName);
+    let scope = this.$rootScope.$new();
+    let attr: string = '';
+
+    if (clean) {
+      this.removeInjectedComponents(target);
+    }
+
+    if (angular.isDefined(attributes) && angular.isObject(attributes)) {
+      attr = this.stringifyAttributes(attributes);
+      scope = angular.merge(scope, attributes);
+    }
+
+    const componentTag = `<${componentTagName} ${attr}>${transclude || ''}</${componentTagName}>`;
+    const element = this.$compile(componentTag)(scope);
+
+    targetEl.append(element);
+  }
+
+  public removeInjectedComponents(target: string | JQuery) {
+    const targetEl = $(target);
+    targetEl.html('');
+  }
+
+  private stringifyAttributes(attributes: any): string {
+    return _.reduce(Object.keys(attributes), (string: string, a: string) => {
+      string += `${a}="${a}"`;
+      return string;
+    }, '');
+  }
+
+  private camelToSnakeCase(name: string): string {
+    return name.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join('-');
+  };
+}
diff --git a/src/app/core/side-panel/side-panel.service.ts b/src/app/core/side-panel/side-panel.service.ts
index 06dfef9..a7471f5 100644
--- a/src/app/core/side-panel/side-panel.service.ts
+++ b/src/app/core/side-panel/side-panel.service.ts
@@ -1,5 +1,5 @@
 import * as $ from 'jquery';
-import * as _ from 'lodash';
+import {IXosComponentInjectorService} from '../services/helpers/component-injector.helpers';
 
 export interface IXosSidePanelService {
   open(): void;
@@ -8,14 +8,15 @@
 }
 
 export class XosSidePanel implements IXosSidePanelService {
-  static $inject = ['$rootScope', '$compile'];
+  static $inject = ['$rootScope', '$compile', 'XosComponentInjector'];
   public sidePanelElName = 'xos-side-panel';
   public sidePanelElClass = '.xos-side-panel';
   public sidePanelEl: JQuery;
 
   constructor (
     private $rootScope: ng.IRootScopeService,
-    private $compile: ng.ICompileService
+    private $compile: ng.ICompileService,
+    private XosComponentInjector: IXosComponentInjectorService
   ) {
     this.sidePanelEl = $(`${this.sidePanelElName} > ${this.sidePanelElClass}`);
   }
@@ -29,36 +30,7 @@
   };
 
   public injectComponent(componentName: string, attributes?: any, transclude?: string) {
-    const componentTagName = this.camelToSnakeCase(componentName);
-    let scope = this.$rootScope.$new();
-    let attr: string = '';
-
-    // NOTE add a flag to keep the loaded compoenents?
-    this.removeInjectedComponents();
-
-    if (angular.isDefined(attributes) && angular.isObject(attributes)) {
-      attr = this.stringifyAttributes(attributes);
-      scope = angular.merge(scope, attributes);
-    }
-
-    const componentTag = `<${componentTagName} ${attr}>${transclude || ''}</${componentTagName}>`;
-    const element = this.$compile(componentTag)(scope);
-    this.sidePanelEl.find('#side-panel-container').append(element);
+    this.XosComponentInjector.injectComponent('#side-panel-container', componentName, attributes, transclude, true);
     this.open();
   }
-
-  public removeInjectedComponents() {
-    this.sidePanelEl.find('#side-panel-container').html('');
-  }
-
-  private stringifyAttributes(attributes: any): string {
-    return _.reduce(Object.keys(attributes), (string: string, a: string) => {
-      string += `${a}="${a}"`;
-      return string;
-    }, '');
-  }
-
-  private camelToSnakeCase(name: string): string {
-    return name.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join('-');
-  };
 }
diff --git a/src/app/views/dashboard/dashboard.html b/src/app/views/dashboard/dashboard.html
index de4c358..f42c842 100644
--- a/src/app/views/dashboard/dashboard.html
+++ b/src/app/views/dashboard/dashboard.html
@@ -1,6 +1,7 @@
 <section class="content">
     <div class="container-fluid">
         <!--<h1>Dashboard</h1>-->
+        <div class="row" id="dashboard-component-container"></div>
         <div class="row">
             <div class="col-xs-4">
                 <div class="panel panel-filled">
@@ -9,7 +10,11 @@
                             {{vm.nodes}}
                             <!--<span class="slight"><i class="fa fa-play fa-rotate-270 text-warning"> </i> +20%</span>-->
                         </h2>
-                        <h4>Nodes</h4>
+                        <h4>
+                            <a ui-sref="xos.core.nodes">
+                                Nodes
+                            </a>
+                        </h4>
                         <!--<div class="small">% New Sessions</div>-->
                         <!--<div class="slight m-t-sm"><i class="fa fa-clock-o"> </i> Updated: <span class="c-white">10:22pm</span></div>-->
                     </div>
@@ -22,7 +27,11 @@
                         <h2 class="m-b-none">
                             {{vm.slices}}
                         </h2>
-                        <h4>Slices</h4>
+                        <h4>
+                            <a ui-sref="xos.core.slices">
+                                Slices
+                            </a>
+                        </h4>
                     </div>
                 </div>
             </div>
@@ -33,7 +42,11 @@
                         <h2 class="m-b-none">
                             {{vm.instances}}
                         </h2>
-                        <h4>Instances</h4>
+                        <h4>
+                            <a ui-sref="xos.core.instances">
+                                Instances
+                            </a>
+                        </h4>
                     </div>
                 </div>
             </div>