How to use mangos own login to secure an AngularJS custom dashboard
-
I believe it was mentioned in your new tutorials that one could use the mdAdmin page as a template which is secured to only allow logged in users, but how would you do the same functionality for your own dashboard? The login page itself seems to only contain some beautiful eye candy and an AngularJS element called <login>, So I should be able to use it as is in my own dashboard, but the thing I'm confused about is how to make the page require it.
If there is an existing demo that demonstrates how to do this I would be glad if someone pointed me to it.
-
Hi nyoa, we added a bare-bones admin template in a folder called adminTemplate. This might be a good starting point for you. Copy the folder to overrides and rename it. You will need to change the require line at the bottom of index.html to suit whatever you renamed the folder to e.g.
<script>require(['dashboards/myAwesomeApplication/app']);</script>
Some of this info is located in the dashboards help under Basics > Create a dashboard but I think I might need to add some details.
You will see in that folder that there are two files related to the login page, views/login.html and directives/login/login.html, you can customise these to suit your needs.
If you want to start from scratch, the way the adminTemplate login works is as follows.
- We use Angular UI Router 0.2.x
- We setup states in the config block using $stateProvider
- All dashboard states have a parent state 'dashboard' which retrieves the current user e.g.
resolve: { auth: ['$rootScope', 'User', function($rootScope, User) { $rootScope.user = User.current(); return $rootScope.user.$promise; }] }
- We listen for an error when changing states, this occurs we can't retrieve the user in above block i.e. the user isn't logged in
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) { if (error && (error.status === 401 || error.status === 403)) { event.preventDefault(); $state.loginRedirect = toState; $state.go('login'); } });
- This redirects the user to the login state which displays the login.html view
-
I was able to implement this in my app by hacking up the adminTemplate to my needs.
However, it would be nice, on logout to redirect to the dashboard login view instead of the mango automation login page. Is there a way to override the existing logout function to achieve this?
-
If I understand your question correctly the answer is, yes. In Mango System Settings -> Dashboard Settings
Fill out the Login page field.
-
The path to your Login page can be a little tricky. In my case I have placed my custom login page in <Mango>/overrides/web/modules/dashboards/web/public/signin.htm
The path I use in my Dashboard Setting is /public-dashboards/signin.htm
-
Thank you for your fast replies. I followed Jareds advice and copied the files inside admin template to my dashboard folder at C:\Program Files\mango automation\overrides\web\modules\dashboards\web\FosfaattiFosfori
I also copied the head part of the template index.html to my own index.html and copied the modified require to the bottom of the file. I didn't quite get it to work with just this so there must be something I missed. It would be great if a custom dashboard login example was added to the tutorials! You have been doing a great job thus far!
Sometimes when not logged in it redirects me to the login page which is empty and sometimes the login page just loads my index data and displays it. It's quite weird.
Here is my index.html so you can check if there is something obviously wrong. I didn't touch either of the login files other than copying.
<!DOCTYPE html> <html lang="en" ma-app="maMaterialDashboards" class="no-js"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>JAMK Fosfaattianalysaattori</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes"> <link rel="icon" type="image/png" sizes="192x192" href="../img/icon192.png"> <link rel="icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="apple-touch-icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="apple-touch-icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="manifest" href="manifest.json"> <link rel="stylesheet" type="text/css" href="tyylit.css"> <link rel="stylesheet" href="/resources/angular-csp.css"></link> <link rel="stylesheet" href="../vendor/angular-material/angular-material.css"> <link rel="stylesheet" href="../vendor/angular-loading-bar/loading-bar.css"> <link rel="stylesheet" href="../vendor/material-design-icons/iconfont/material-icons.css"> <link rel="stylesheet" href="../vendor/font-awesome/css/font-awesome.css"> <link rel="stylesheet" href="../vendor/mdPickers/mdPickers.css"> <link rel="stylesheet" href="../vendor/angular-material-data-table/md-data-table.css"> <link rel="stylesheet" href="styles/main.css"> </head> <body layout="column"> <div ng-if="appLoading"> <img src="images/loader.gif" alt="App loading.." style="margin-left:50%;margin-top:25%"> </div> <div id="maincontainer" ng-cloak> <header> <img src="images/jamkfi_tunnus_sininen_suomi.png" alt="jamk.fi" style="width:50%;height:50%;margin-left:25%;"> </header> <div id="textContainer"> <img src="images/header.jpg" alt="Fosfaattifosfori PO4-P [µg/l]" style="width:50%;height:50%;margin-left:25%;"> </div> <div> <ma-get-point-value point-xid="DP_355369" point="point1"></ma-get-point-value> <ma-point-values id="testi" point="point1" values="point1Values" from="from" to="to" rollup="AVERAGE" rollup-interval="1 minutes"> </ma-point-values> <ma-serial-chart id="chartti" style="height: 500px; width: 100%" series-1-values="point1Values" series-1-point="point1" options="{chartCursor:{categoryBalloonDateFormat:'YYYY-MM-DD HH:NN'}, chartScrollbar:{oppositeAxis: false, scrollbarHeight: 40,offset: 25, graph:'DP_355369'}}"> </ma-serial-chart> <div id = "control"> <div id="selectorDiv" layout="row"> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>Date preset</label> <ma-date-range-picker from="from" to="to" preset="LAST_1_DAYS" update-interval="1 minutes"></ma-date-range-picker> </md-input-container> </div> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>From date</label> <ma-date-picker ng-model="from"></ma-date-picker> </md-input-container> </div> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>To date</label> <ma-date-picker ng-model="to"></ma-date-picker> </md-input-container> </div> </div> <div> <div> <md-button class="md-raised" ng-click="showStats=true;showData=false" ng-hide="showStats">Statistics</md-button> <md-button class="md-raised" ng-click="showStats=false;showData=false" ng-show="showStats">Statistics</md-button> <md-button class="md-raised" ng-click="showData=true;showStats=false" ng-hide="showData">Data</md-button> <md-button class="md-raised" ng-click="showData=false;showStats=false" ng-show="showData">Data</md-button> </div> <div ng-controller="myCtrl"> <md-button class="md-raised" ng-click="testi()">Push me</md-button> </div> <div layout="row" ng-show="showStats" class="ng-hide"> <ma-point-statistics point="point1" from="from" to="to" statistics="statsObj"></ma-point-statistics> <ma-statistics-table id="statsTable" statistics="statsObj"></ma-statistics-table> </div> <div layout="row" ng-show="showData" class="ng-hide"> <table width="100%" height="500px"> <thead> <tr> <th>Value</th> <th>Time</th> </tr> </thead> <tbody> <tr ng-repeat="item in point1Values"> <td>{{item.value}}</td> <td>{{item.timestamp | moment:'format':'lll' }}</td> </tr> </tbody> </table> </div> </div> </div> </div> </div> <script src="https://rawgithub.com/eligrey/FileSaver.js/master/FileSaver.js" type="text/javascript"></script> <script src="/resources/require.js"></script> <script src="/resources/loaderConfig.js"></script> <script src="../js/loaderConfig.js"></script> <script>require(['dashboards/fosfaattifosfori/app']);</script> <script type="text/javascript">require(['mango-3.0/bootstrap']);</script> <script type="text/javascript">require(['dashboards/fosfaattifosfori/export']);</script> </body> </html>
I'll upload a picture of my custom dashboard directory so you can tell if I've copied the right things. Thanks for all the help!
-
OK, couple of things
- I'm not sure what export.js is doing
- You shouldn't use 'mango-3.0/bootstrap' if you are bootstrapping your own app inside app.js
- If the login page is coming up blank then you are probably not loading the login directive correctly inside app.js
- You need to change the login page as per Woody's instructions above if you want to use the custom dashboard login page as the standard login page for the whole of Mango
In short you will need to post up the contents of export.js and app.js for us to help you some more.
-
Thank you a lot. I've used your templates for creating the dashboard so 'mango-3.0/bootstrap' was a remnant from before copying app.js over. The login page isn't required to be standard for mango so it should be for the dashboard only.
export.js is there for my custom directives. I'll copy it's content here
/** * @copyright 2016 {@link http://infiniteautomation.com|Infinite Automation Systems, Inc.} All rights reserved. * @author Jared Wiltshire */ define([ 'angular', 'mango-3.0/maMaterialDashboards' ], function(angular, maMaterialDashboards) { 'use strict'; var myApp = angular.module('myApp', ['maMaterialDashboards']); myApp.run(['$rootScope', function($rootScope) { $rootScope.pi = function() { return Math.PI; } }]); myApp.directive('myCustomComponent', function() { return { restrict: 'E', scope: { name: '@' }, template: '<span>Hello {{name}}!</span>' }; }); myApp.controller('myCtrl',['$scope', $scope.exportData = function(values){ $scope.list = []; for (var i = 0, len = values.length; i < len; i++) { $scope.d = new Date(values*.timestamp); $scope.list.splice(0,0,{"timestamp":$scope.d.toLocaleString(),"value":values*.value}); }; }; }]); angular.element(document).ready(function() { angular.bootstrap(document.documentElement, ['myApp']); }); }); // define
I haven't done any changes to app.js, although it looks like I should change the "state: 'dashboard.home'" to point to the directory I have my index.html in?
Here is the content of app.js
/** * @copyright 2016 {@link http://infiniteautomation.com|Infinite Automation Systems, Inc.} All rights reserved. * @author Jared Wiltshire */ define([ 'angular', './directives/menu/menuLink', './directives/menu/menuToggle', './directives/login/login', 'mango-3.0/maMaterialDashboards', 'mango-3.0/maAppComponents', 'angular-ui-router', 'angular-loading-bar' ], function(angular, menuLink, menuToggle, login, maMaterialDashboards, maAppComponents) { 'use strict'; var myAdminApp = angular.module('myAdminApp', [ 'ui.router', 'angular-loading-bar', 'maMaterialDashboards', 'maAppComponents', 'ngMessages' ]); myAdminApp .directive('menuLink', menuLink) .directive('menuToggle', menuToggle) .directive('login', login); myAdminApp.constant('PAGES', [ { state: 'dashboard', url: '/dashboard', templateUrl: 'views/dashboard/main.html', resolve: { auth: ['$rootScope', 'User', function($rootScope, User) { $rootScope.user = User.current(); return $rootScope.user.$promise; }] } }, { state: 'login', url: '/login', templateUrl: 'views/login.html' }, { state: 'dashboard.home', url: '/home', templateUrl: 'views/dashboard/home.html', menuTr: 'dashboards.v3.dox.home', menuIcon: 'fa fa-home', menuType: 'link' }, { state: 'dashboard.apiErrors', url: '/api-errors', templateUrl: 'views/dashboard/errors.html', menuTr: 'dashboards.v3.dox.apiErrors' }, { state: 'dashboard.section1', url: '/section-1', menuText: 'Section 1', menuIcon: 'fa fa-building', menuType: 'toggle', children: [ { state: 'dashboard.section1.page1', templateUrl: 'views/section1/page1.html', url: '/page-1', menuText: 'Page 1', menuType: 'link' }, { state: 'dashboard.section1.page2', templateUrl: 'views/section1/page2.html', url: '/page-2', menuText: 'Page 2', menuType: 'link' } ] }, { state: 'dashboard.section2', url: '/section-2', menuText: 'Section 2', menuIcon: 'fa fa-bolt', menuType: 'toggle', children: [ { state: 'dashboard.section2.page1', templateUrl: 'views/section2/page1.html', url: '/page-1', menuText: 'Page 1', menuType: 'link' }, { state: 'dashboard.section2.page2', templateUrl: 'views/section2/page2.html', url: '/page-2', menuText: 'Page 2', menuType: 'link' } ] } ]); myAdminApp.config([ 'PAGES', '$stateProvider', '$urlRouterProvider', '$httpProvider', '$mdThemingProvider', '$injector', function(PAGES, $stateProvider, $urlRouterProvider, $httpProvider, $mdThemingProvider, $injector) { $mdThemingProvider .theme('default') .primaryPalette('yellow') .accentPalette('red'); $httpProvider.interceptors.push('errorInterceptor'); $urlRouterProvider.otherwise('/dashboard/home'); addStates(PAGES); function addStates(pages, parent) { angular.forEach(pages, function(page, area) { if (page.state) { var state = { url: page.url } if (page.menuTr) { state.menuTr = page.menuTr; } if (page.menuText) { state.menuText = page.menuText; } if (parent) { state.parentPage = parent; } if (page.templateUrl) { state.templateUrl = page.templateUrl; } else { state.template = '<div ui-view></div>'; state['abstract'] = true; } if (page.resolve) { state.resolve = page.resolve; } $stateProvider.state(page.state, state); } addStates(page.children, page); }); } }]); myAdminApp.run([ 'PAGES', '$rootScope', '$state', '$timeout', '$mdSidenav', '$mdColors', '$MD_THEME_CSS', function(PAGES, $rootScope, $state, $timeout, $mdSidenav, $mdColors, $MD_THEME_CSS) { $rootScope.pages = PAGES; $rootScope.Math = Math; // inserts a style tag to style <a> tags with accent color if ($MD_THEME_CSS) { var acc = $mdColors.getThemeColor('accent-500-1.0'); var accT = $mdColors.getThemeColor('accent-500-0.2'); var accD = $mdColors.getThemeColor('accent-700-1.0'); var styleContent = 'a:not(.md-button) {color: ' + acc +'; border-bottom-color: ' + accT + ';}\n' + 'a:not(.md-button):hover, a:not(.md-button):focus {color: ' + accD + '; border-bottom-color: ' + accD + ';}\n'; var style = document.createElement('style'); style.appendChild(document.createTextNode(styleContent)); document.head.appendChild(style); } $rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) { if (error && (error.status === 401 || error.status === 403)) { event.preventDefault(); $state.loginRedirect = toState; $state.go('login'); } }); $rootScope.$on("$stateChangeSuccess", function(event, toState, toParams, fromState, fromParams) { var crumbs = []; var state = toState; do { if (state.menuTr) { crumbs.unshift({maTr: state.menuTr}); } else if (state.menuText) { crumbs.unshift({text: state.menuText}); } } while (state = state.parentPage); $rootScope.crumbs = crumbs; }); $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { if ($state.includes('dashboard')) { $rootScope.closeMenu(); } }); $rootScope.closeMenu = function() { $mdSidenav('left').close(); } $rootScope.openMenu = function() { angular.element('#menu-button').blur(); $mdSidenav('left').open(); } }]); angular.element(document).ready(function() { angular.bootstrap(document.documentElement, ['myAdminApp']); }); }); // define
-
So what you need to do is to combine the contents of export.js and app.js, at the moment you are trying to bootstrap two separate AngularJS apps on the same element (document.documentElement) which wont work.
Either migrate your controller to app.js and remove export.js or migrate the login stuff as per my previous post into export.js and remove app.js. Decided which way to go based on whether you wish to keep the menu and toolbar or not.
-
Here is my app.js if it is of any assistance:
/** * @copyright 2016 {@link http://infiniteautomation.com|Infinite Automation Systems, Inc.} All rights reserved. * @author Jared Wiltshire */ define([ 'angular', './directives/login/login', 'mango-3.0/maMaterialDashboards', 'mango-3.0/maAppComponents', 'angular-ui-router', 'angular-loading-bar' ], function(angular, login, maMaterialDashboards, maAppComponents) { 'use strict'; var myApp = angular.module('myApp', [ 'ui.router', 'angular-loading-bar', 'maMaterialDashboards', 'maAppComponents', 'ngMessages' ]); myApp .directive('login', login); myApp.constant('PAGES', [ { state: 'dashboard', url: '/dashboard', templateUrl: 'views/dashboard/main.html', resolve: { auth: ['$rootScope', 'User', function($rootScope, User) { $rootScope.user = User.current(); return $rootScope.user.$promise; }] } }, { state: 'login', url: '/login', templateUrl: 'views/login.html' }, { state: 'dashboard.home', url: '/home', templateUrl: 'views/dashboard/home.html' } ]); myApp.config([ 'PAGES', '$stateProvider', '$urlRouterProvider', '$httpProvider', '$mdThemingProvider', '$injector', function(PAGES, $stateProvider, $urlRouterProvider, $httpProvider, $mdThemingProvider, $injector) { $mdThemingProvider.theme('default') .dark() .primaryPalette('orange') .accentPalette('light-blue') .warnPalette('blue'); $httpProvider.interceptors.push('errorInterceptor'); $urlRouterProvider.otherwise('/dashboard/home'); addStates(PAGES); function addStates(pages, parent) { angular.forEach(pages, function(page, area) { if (page.state) { var state = { url: page.url } if (parent) { state.parentPage = parent; } if (page.templateUrl) { state.templateUrl = page.templateUrl; } else { state.template = '<div ui-view></div>'; state['abstract'] = true; } if (page.resolve) { state.resolve = page.resolve; } $stateProvider.state(page.state, state); } addStates(page.children, page); }); } }]); myApp.run([ 'PAGES', '$rootScope', '$state', '$timeout', '$mdSidenav', '$mdColors', '$MD_THEME_CSS', function(PAGES, $rootScope, $state, $timeout, $mdSidenav, $mdColors, $MD_THEME_CSS) { $rootScope.pages = PAGES; $rootScope.Math = Math; $rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) { if (error && (error.status === 401 || error.status === 403)) { event.preventDefault(); $state.loginRedirect = toState; $state.go('login'); } }); }]); angular.element(document).ready(function() { angular.bootstrap(document.documentElement, ['myApp']); }); }); // define
I took out all of the menu portions for the side nav and the extra pages. My dashboard is only one page at the moment.
-
Thank you a lot for your reference. I tried copying your app.js and see if it would work. It does the redirect to /#/login when the user is not logged in but the page shows my index.html for some reason. I'll upload a picture about the situation.
My index.html is the same it was before with the exception that the only require line I have includes the app.js.
-
Here is how my pages are structured:
index.html
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>Demo</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes"> <link rel="icon" type="image/png" sizes="192x192" href="../img/icon192.png"> <link rel="icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="apple-touch-icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="apple-touch-icon" type="image/png" sizes="128x128" href="../img/icon128.png"> <link rel="manifest" href="manifest.json"> <link rel="stylesheet" href="/resources/angular-csp.css"></link> <link rel="stylesheet" href="../vendor/angular-material/angular-material.css"> <link rel="stylesheet" href="../vendor/angular-loading-bar/loading-bar.css"> <link rel="stylesheet" href="../vendor/material-design-icons/iconfont/material-icons.css"> <link rel="stylesheet" href="../vendor/font-awesome/css/font-awesome.css"> <link rel="stylesheet" href="../vendor/mdPickers/mdPickers.css"> <link rel="stylesheet" href="../vendor/angular-material-data-table/md-data-table.css"> <link rel="stylesheet" href="styles/main.css"> </head> <body layout="column"> <div ng-if="appLoading" class="app-loading"> <i class="fa fa-cog fa-spin"></i> </div> <div ui-view ng-cloak layout="column" flex></div> <script src="/resources/require.js"></script> <script src="/resources/loaderConfig.js"></script> <script src="../js/loaderConfig.js"></script> <script>require(['dashboards/Demo/app']);</script> </body> </html>
main.html
<div ui-view flex="noshrink"></div>
You could do away with the main.html if you rework the code a bit, but I expect to use it further along in my dashboard development.
The home.html page holds all of my dashboard content. So in order to make it work with my app.js, copy the <body> portion of your index.html into /views/dashboard/home.html and reduce the main.html to something similar to above.
-
Your home.html would look something like this:
<div id="maincontainer" ng-cloak> <header> <img src="images/jamkfi_tunnus_sininen_suomi.png" alt="jamk.fi" style="width:50%;height:50%;margin-left:25%;"> </header> <div id="textContainer"> <img src="images/header.jpg" alt="Fosfaattifosfori PO4-P [µg/l]" style="width:50%;height:50%;margin-left:25%;"> </div> <div> <ma-get-point-value point-xid="DP_355369" point="point1"></ma-get-point-value> <ma-point-values id="testi" point="point1" values="point1Values" from="from" to="to" rollup="AVERAGE" rollup-interval="1 minutes"> </ma-point-values> <ma-serial-chart id="chartti" style="height: 500px; width: 100%" series-1-values="point1Values" series-1-point="point1" options="{chartCursor:{categoryBalloonDateFormat:'YYYY-MM-DD HH:NN'}, chartScrollbar:{oppositeAxis: false, scrollbarHeight: 40,offset: 25, graph:'DP_355369'}}"> </ma-serial-chart> <div id = "control"> <div id="selectorDiv" layout="row"> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>Date preset</label> <ma-date-range-picker from="from" to="to" preset="LAST_1_DAYS" update-interval="1 minutes"></ma-date-range-picker> </md-input-container> </div> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>From date</label> <ma-date-picker ng-model="from"></ma-date-picker> </md-input-container> </div> <div class="selectorContainer"> <md-input-container flex="33" class="no-errors-spacer timeSelector"> <label>To date</label> <ma-date-picker ng-model="to"></ma-date-picker> </md-input-container> </div> </div> <div> <div> <md-button class="md-raised" ng-click="showStats=true;showData=false" ng-hide="showStats">Statistics</md-button> <md-button class="md-raised" ng-click="showStats=false;showData=false" ng-show="showStats">Statistics</md-button> <md-button class="md-raised" ng-click="showData=true;showStats=false" ng-hide="showData">Data</md-button> <md-button class="md-raised" ng-click="showData=false;showStats=false" ng-show="showData">Data</md-button> </div> <div ng-controller="myCtrl"> <md-button class="md-raised" ng-click="testi()">Push me</md-button> </div> <div layout="row" ng-show="showStats" class="ng-hide"> <ma-point-statistics point="point1" from="from" to="to" statistics="statsObj"></ma-point-statistics> <ma-statistics-table id="statsTable" statistics="statsObj"></ma-statistics-table> </div> <div layout="row" ng-show="showData" class="ng-hide"> <table width="100%" height="500px"> <thead> <tr> <th>Value</th> <th>Time</th> </tr> </thead> <tbody> <tr ng-repeat="item in point1Values"> <td>{{item.value}}</td> <td>{{item.timestamp | moment:'format':'lll' }}</td> </tr> </tbody> </table> </div> </div> </div> </div> </div>
-
Thank you a lot brad! Thanks to your reference I got it working. I really can't thank you enough for sharing your experience here!
I also got a lot clearer idea of the new dashboard module thanks to this discussion.
So to summarize all the steps that a newbie like me had to take to get authentication work in my custom dashboard:
- Copy the files in adminTemplate (found at {mango root}\web\modules\dashboards\web) to my custom dashboard directory
- Change the content of my app.js directory to reflect brads.
- Copy the html data between the <body> tags in my old index.html to home.html found in views/dasboard/ directory
- Replace my old index.html file with adminTemplate index.html with the require line modified to point to my dashboard (eg. <script>require(['dashboards/fosfaattifosfori/app']);</script>)
- Changed my main.html to reflect what brad posted earlier
I believe steps 2 and 5 aren't necessary to get the thing working, but without those I'd have the side nav and the extra pages.
Thank you a lot for all the help you provided!
-
Thanks for posting up your pages and helping out Brad!
I've added an example for a simple "Single Page App" with a login page using UI Router to the next version of the dashboards module. You can have a look here for reference in the meantime - https://github.com/infiniteautomation/ma-dashboards/tree/master/Custom Dashboards/web/loginPageTemplate
-
@Jared-Wiltshire the link https://github.com/infiniteautomation/ma-dashboards/tree/master/Custom Dashboards/web/loginPageTemplate isn't working atm :)
-