From 08680403c1ab72b7789517426293681c2224eada Mon Sep 17 00:00:00 2001 From: Dave Bauman Date: Thu, 7 Jul 2016 16:42:20 -0400 Subject: [PATCH] 1.34.0 release --- CHANGELOG.md | 20 +++ cyclotron-site/app/partials/dashboard.jade | 12 +- .../app/partials/dashboardSidebar.jade | 42 +++++ cyclotron-site/app/partials/header.jade | 2 +- .../app/partials/help/3rdparty.jade | 2 +- .../app/partials/help/javascriptApi.jade | 12 ++ cyclotron-site/app/partials/home.jade | 2 +- .../app/partials/sidebarAccordion.jade | 1 + .../app/partials/sidebarAccordionGroup.jade | 8 + cyclotron-site/app/scripts/common/app.coffee | 7 +- .../app/scripts/common/mixins.coffee | 3 +- .../services.commonConfigService.coffee | 74 +++++++- .../services/services.dashboardService.coffee | 10 ++ .../dashboards/controller.dashboard.coffee | 124 +++++++------ .../directives/directives.dashboard.coffee | 108 ++++++++++++ .../directives.dashboardPage.coffee | 90 ++-------- .../directives.dashboardSidebar.coffee | 116 +++++++++++++ .../directives.dashboardWidget.coffee | 1 + .../directives.sidebarAccordion.coffee | 144 ++++++++++++++++ .../directives/directives.widget.coffee | 132 +++++++------- .../directives/directives.widgetBody.coffee | 4 +- .../directives/directives.widgetError.coffee | 4 +- .../services.dashboardOverridesService.coffee | 71 ++++++++ .../scripts/mgmt/controller.guiEditor.coffee | 10 +- .../directives.metricsGraphics.coffee | 4 +- cyclotron-site/app/styles/common/main.less | 4 + .../app/styles/common/variables.less | 35 ++++ .../app/styles/dashboards/_sidebar.less | 163 ++++++++++++++++++ .../app/styles/dashboards/dashboard.less | 27 +++ cyclotron-site/app/styles/themes/dark.less | 26 ++- cyclotron-site/app/styles/themes/dark2.less | 26 ++- cyclotron-site/app/styles/themes/gto.less | 12 +- cyclotron-site/app/styles/themes/light.less | 17 ++ .../app/styles/themes/lightborderless.less | 15 ++ cyclotron-site/app/widgets/table/_table.less | 6 +- .../table/directives.tableColumn.coffee | 4 +- .../table/directives.tableFixedHeader.coffee | 55 +++--- cyclotron-site/bower.json | 6 +- cyclotron-site/gulpfile.coffee | 14 +- cyclotron-site/package.json | 2 +- cyclotron-site/test/unit/mixins-spec.coffee | 3 + .../test/unit/mixins-varsub-spec.coffee | 3 + cyclotron-svc/package.json | 2 +- 43 files changed, 1158 insertions(+), 265 deletions(-) create mode 100644 cyclotron-site/app/partials/dashboardSidebar.jade create mode 100644 cyclotron-site/app/partials/sidebarAccordion.jade create mode 100644 cyclotron-site/app/partials/sidebarAccordionGroup.jade create mode 100644 cyclotron-site/app/scripts/dashboards/directives/directives.dashboard.coffee create mode 100644 cyclotron-site/app/scripts/dashboards/directives/directives.dashboardSidebar.coffee create mode 100644 cyclotron-site/app/scripts/dashboards/directives/directives.sidebarAccordion.coffee create mode 100644 cyclotron-site/app/scripts/dashboards/services/services.dashboardOverridesService.coffee create mode 100644 cyclotron-site/app/styles/dashboards/_sidebar.less diff --git a/CHANGELOG.md b/CHANGELOG.md index 1737bc1..e4d4d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 1.34.0 (07/07/2016) + +## Features + + - Dashboard Sidebar: Added a new sidebar with a toolbar of relevent links, ability to show/hide Widgets, and space for Dashboard-defined content to appear. Dashboards can use this space to provide useful information about the data or visualizations, or to add controls, filters, etc. + + - Cyclotron.functions.forceUpdate: Added method to manually trigger an Angular.js digest cycle. + + - Upgraded Moment.js to 2.13.0 + +## Bug Fixes + + - Dashboard Performance: improved dashboard performance across the board + + - Table Widget: fixed broken Freeze Headers functionality + + - Header Widget: when displaying page name in the header, value was always blank + + - Fixed Numeral formatting errors with non-numeric strings; added unit tests + # 1.33.0 (06/23/2016) ## Features diff --git a/cyclotron-site/app/partials/dashboard.jade b/cyclotron-site/app/partials/dashboard.jade index 70ad369..ca80902 100644 --- a/cyclotron-site/app/partials/dashboard.jade +++ b/cyclotron-site/app/partials/dashboard.jade @@ -1,6 +1,10 @@ //- Dashboards -div.dashboard +div.dashboard.dashboard-page-background(ng-class='{ "has-sidebar": dashboard.sidebar.showDashboardSidebar }') + + .click-cover + + include dashboardSidebar .dashboard-controls(ng-hide='dashboard.showDashboardControls == false') i.fa.fa-chevron-left(ng-click='moveBack()', ng-class='{ disabled: !canMoveBack() }', title='Go to the previous page') @@ -12,7 +16,7 @@ div.dashboard i.fa.fa-chevron-right(ng-click='moveForward()', ng-class='{ disabled: !canMoveForward() }', title='Go to the next page') .dashboard-pages - div(dashboard-page, dashboard='dashboard', - page='page', page-number='{{ currentPageIndex }}', page-overrides='dashboardOverrides.pages[currentPageIndex]', - ng-repeat='page in currentPage') + div(dashboard-page, ng-repeat='page in currentPage', + dashboard='dashboard', page='page', page-number='{{ currentPageIndex }}', + page-overrides='dashboardOverrides.pages[currentPageIndex]') diff --git a/cyclotron-site/app/partials/dashboardSidebar.jade b/cyclotron-site/app/partials/dashboardSidebar.jade new file mode 100644 index 0000000..e53e675 --- /dev/null +++ b/cyclotron-site/app/partials/dashboardSidebar.jade @@ -0,0 +1,42 @@ +.dashboard-sidebar.collapsed.dashboard-page-background(ng-if='dashboard.sidebar.showDashboardSidebar') + .sidebar-expander-hitbox + .sidebar-expander + i.fa.fa-caret-right + + .sidebar-header + h1(ng-if='dashboard.sidebar.showDashboardTitle == true') {{ dashboardDisplayName }} + table.iconbar(ng-if='dashboard.sidebar.showToolbar == true') + tr + td + a(ng-href='/edit/{{ dashboard.name }}', title='Edit this Dashboard', target='_blank') + i.fa.fa-edit + td(ng-if='analyticsEnabled()') + a(ng-href='/analytics/{{ dashboard.name }}', title='View Analytics for this Dashboard', target='_blank') + i.fa.fa-bar-chart + td + a(ng-href='{{ exportUrl }}', title='Export this Dashboard', target='_blank') + i.fa.fa-download + td(requires-auth) + i.fa.fa-thumbs-o-up(ng-if='!isLiked', ng-click='toggleLike()', title='Like this Dashboard') + i.fa.fa-thumbs-up(ng-if='isLiked', ng-click='toggleLike()', title='Unlike this Dashboard') + + .sidebar-accordion + + accordion-group(ng-repeat='content in dashboard.sidebar.sidebarContent track by $index', heading='{{ content.heading }}') + div(ng-bind-html='trustHtml(content.html)') + + accordion-group(heading='Show/Hide Widgets', ng-if='dashboard.sidebar.showHideWidgets == true') + table.table + tr(ng-repeat='widget in widgetVisibilities track by $index') + td {{ widget.label }} + td + switch(ng-model='widget.visible', ng-change='changeVisibility(widget, $index)') + p.centered + a(ng-click='resetDashboardOverrides()', title='Reset to default') + i.fa.fa-refresh + | Reset to default + .sidebar-footer + .logos + a.logo(ng-repeat='logo in footerLogos', title='{{ logo.title }}', + ng-href='{{ logo.href }}', target='_self') + img(ng-src='{{ logo.src }}', alt='{{ logo.title }}') diff --git a/cyclotron-site/app/partials/header.jade b/cyclotron-site/app/partials/header.jade index fd38b55..ece4448 100644 --- a/cyclotron-site/app/partials/header.jade +++ b/cyclotron-site/app/partials/header.jade @@ -9,7 +9,7 @@ i.fa.fa-file-o a.documentation(ui-sref='help', target='_blank' title='Documentation for Cyclotron') i.fa.fa-question-circle - a.analytics(ui-sref='analytics', title='Analytics for Cyclotron') + a.analytics(ng-if='analyticsEnabled()', ui-sref='analytics', title='Analytics for Cyclotron') i.fa.fa-bar-chart a.login(requires-auth, ng-click='login()', ng-if='!isLoggedIn()', title='Login') i.fa.fa-unlock diff --git a/cyclotron-site/app/partials/help/3rdparty.jade b/cyclotron-site/app/partials/help/3rdparty.jade index c2734bc..a5448b4 100644 --- a/cyclotron-site/app/partials/help/3rdparty.jade +++ b/cyclotron-site/app/partials/help/3rdparty.jade @@ -28,7 +28,7 @@ table tr td a(href='http://momentjs.com/', target='_blank') Moment.js - td 2.8.4 + td 2.13.0 td Parse, validate, manipulate, and display dates in javascript tr td diff --git a/cyclotron-site/app/partials/help/javascriptApi.jade b/cyclotron-site/app/partials/help/javascriptApi.jade index 21ac99d..ec9b4b8 100644 --- a/cyclotron-site/app/partials/help/javascriptApi.jade +++ b/cyclotron-site/app/partials/help/javascriptApi.jade @@ -35,6 +35,18 @@ table td Cyclotron.getDeeplink() td Returns a deeplink URL to the current Dashboard, including the values of all Parameters +h4 Functions + +p These functions are hooks into Cyclotron that can be leveraged by Dashboards + +table + tr + th Function + th Description + tr + td Cyclotron.functions.forceUpdate() + td Forces Cyclotron to do an internal update of Dashboard state, e.g. syncing parameters to the URL. In general, this should not be needed, but can be used to immediately trigger an update cycle after running custom JavaScript. + h4 Built-In Parameters p These Parameters are built-in to every Dashboard, and appear in the URL when set. They don't have to be configured manually in the Parameters section of the Dashboard, but they can be added there in order to change the default value. diff --git a/cyclotron-site/app/partials/home.jade b/cyclotron-site/app/partials/home.jade index 0620193..fb72c2b 100644 --- a/cyclotron-site/app/partials/home.jade +++ b/cyclotron-site/app/partials/home.jade @@ -37,7 +37,7 @@ a.documentation(ui-sref='help', title='Documentation for Cyclotron') i.fa.fa-question-circle span Help - a.login(ui-sref='analytics', title='Analytics for Cyclotron') + a.login(ng-if='analyticsEnabled()', ui-sref='analytics', title='Analytics for Cyclotron') i.fa.fa-bar-chart span Analytics a.login(requires-auth, ng-if='!isLoggedIn()', ng-click='login()',title='Login') diff --git a/cyclotron-site/app/partials/sidebarAccordion.jade b/cyclotron-site/app/partials/sidebarAccordion.jade new file mode 100644 index 0000000..34f4e04 --- /dev/null +++ b/cyclotron-site/app/partials/sidebarAccordion.jade @@ -0,0 +1 @@ +.panel-group(ng-transclude='') diff --git a/cyclotron-site/app/partials/sidebarAccordionGroup.jade b/cyclotron-site/app/partials/sidebarAccordionGroup.jade new file mode 100644 index 0000000..dd3d3d0 --- /dev/null +++ b/cyclotron-site/app/partials/sidebarAccordionGroup.jade @@ -0,0 +1,8 @@ +.panel.panel-default + .panel-heading(ng-click='toggleOpen()') + h4.panel-title + a.accordion-toggle(accordion-transclude='heading') + span(ng-class='{"text-muted": isDisabled}') {{heading}} + + .panel-collapse(uib-collapse='!isOpen') + .panel-body(ng-style='styles', ng-transclude='') diff --git a/cyclotron-site/app/scripts/common/app.coffee b/cyclotron-site/app/scripts/common/app.coffee index 92d161c..7c1ab44 100644 --- a/cyclotron-site/app/scripts/common/app.coffee +++ b/cyclotron-site/app/scripts/common/app.coffee @@ -48,9 +48,10 @@ cyclotronApp = angular.module 'cyclotronApp', [ 'ui.bootstrap' 'ui.ace' 'drahak.hotkeys' + 'googlechart' 'LocalForageModule' 'tableSort' - 'googlechart' + 'uiSwitch' ] cyclotronDirectives = angular.module 'cyclotronApp.directives', [] @@ -309,7 +310,7 @@ cyclotronApp.config ($stateProvider, $urlRouterProvider, $locationProvider, $con } $locationProvider.hashPrefix = '!' -cyclotronApp.run ($rootScope, $urlRouter, $location, $state, $stateParams, $uibModal, userService) -> +cyclotronApp.run ($rootScope, $urlRouter, $location, $state, $stateParams, $uibModal, configService, userService) -> # # Authentication-related scope variables @@ -318,6 +319,8 @@ cyclotronApp.run ($rootScope, $urlRouter, $location, $state, $stateParams, $uibM $rootScope.isAdmin = userService.isAdmin $rootScope.currentUser = userService.currentUser + $rootScope.analyticsEnabled = -> configService.enableAnalytics + $rootScope.login = (isModal = false) -> options = templateUrl: '/partials/login.html' diff --git a/cyclotron-site/app/scripts/common/mixins.coffee b/cyclotron-site/app/scripts/common/mixins.coffee index 15e432e..d12903f 100644 --- a/cyclotron-site/app/scripts/common/mixins.coffee +++ b/cyclotron-site/app/scripts/common/mixins.coffee @@ -168,7 +168,8 @@ _.mixin({ return 'NaN' if _.isNaN value if !_.isNumber(value) - value = parseFloat(value) + parsedValue = parseFloat(value) + if _.isNaN parsedValue then return value else value = parsedValue return numeral(value).format(format) # ngApply: Takes a scope and a function, and returns a function that calls $apply around the given function diff --git a/cyclotron-site/app/scripts/common/services/services.commonConfigService.coffee b/cyclotron-site/app/scripts/common/services/services.commonConfigService.coffee index 0cc5b98..20393a1 100644 --- a/cyclotron-site/app/scripts/common/services/services.commonConfigService.coffee +++ b/cyclotron-site/app/scripts/common/services/services.commonConfigService.coffee @@ -26,7 +26,7 @@ cyclotronServices.factory 'commonConfigService', -> exports = { - version: '1.33.0' + version: '1.34.0' logging: enableDebug: false @@ -171,6 +171,67 @@ cyclotronServices.factory 'commonConfigService', -> default: true defaultHidden: true + sidebar: + label: 'Sidebar' + description: '' + type: 'propertyset' + default: {} + defaultHidden: true + properties: + showDashboardSidebar: + label: 'Show Dashboard Sidebar' + description: 'If false, hides the default Dashboard Sidebar.' + type: 'boolean' + required: false + default: false + order: 10 + showDashboardTitle: + label: 'Include Dashboard Title' + description: 'Enables a section of the sidebar for the Dashboard title.' + type: 'boolean' + required: false + default: true + defaultHidden: true + order: 11 + showToolbar: + label: 'Include Toolbar' + description: 'Enables a toolbar in the sidebar.' + type: 'boolean' + required: false + default: true + defaultHidden: true + order: 12 + showHideWidgets: + label: 'Include Show/Hide Widgets' + description: 'Enables a section of the sidebar for overriding the visibility of Widgets.' + type: 'boolean' + required: false + default: false + defaultHidden: true + order: 13 + sidebarContent: + label: 'Custom Sidebar Sections' + singleLabel: 'Section' + description: 'One or more sections of content to display in the Sidebar.' + type: 'propertyset[]' + default: [] + order: 15 + properties: + heading: + label: 'Heading' + description: 'Heading for the Sidebar section.' + type: 'string' + inlineJs: true + order: 1 + html: + label: 'HTML Content' + description: 'HTML Content to display.' + placeholder: 'Value' + type: 'editor' + editorMode: 'html' + inlineJs: true + order: 2 + pages: label: 'Pages' description: 'The list of Page definitions which compose the Dashboard.' @@ -1065,6 +1126,17 @@ cyclotronServices.factory 'commonConfigService', -> sample: name: '' + sidebar: + showDashboardSidebar: true + + # Dashboard Sidebar + dashboardSidebar: + footer: + logos: [{ + title: 'Cyclotron' + src: '/img/favicon32.png' + href: '/' + }] # List of help pages help: [ diff --git a/cyclotron-site/app/scripts/common/services/services.dashboardService.coffee b/cyclotron-site/app/scripts/common/services/services.dashboardService.coffee index 25481a3..ffba3b5 100644 --- a/cyclotron-site/app/scripts/common/services/services.dashboardService.coffee +++ b/cyclotron-site/app/scripts/common/services/services.dashboardService.coffee @@ -599,6 +599,16 @@ cyclotronServices.factory 'dashboardService', ($http, $resource, $q, analyticsSe else pageName + service.getWidgetName = (widget, index) -> + if !widget.widget? || widget.widget == '' + return 'Widget ' + (index + 1) + if widget?.name?.length > 0 + return _.titleCase(widget.widget) + ': ' + widget.name + else if widget.title?.length > 0 + return _.titleCase(widget.widget) + ': ' + widget.title + else + return _.titleCase(widget.widget) + service.getVisitCategory = (dashboard) -> return null unless dashboard? visits = dashboard.visits diff --git a/cyclotron-site/app/scripts/dashboards/controller.dashboard.coffee b/cyclotron-site/app/scripts/dashboards/controller.dashboard.coffee index 18f1f5c..dba5830 100644 --- a/cyclotron-site/app/scripts/dashboards/controller.dashboard.coffee +++ b/cyclotron-site/app/scripts/dashboards/controller.dashboard.coffee @@ -17,7 +17,7 @@ # # Home controller. # -cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localForage, $location, $timeout, $window, $q, $uibModal, analyticsService, configService, cyclotronDataService, dashboardService, dataService, loadService, logService, parameterService, userService) -> +cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localForage, $location, $timeout, $window, $q, $uibModal, analyticsService, configService, cyclotronDataService, dashboardOverridesService, dashboardService, dataService, loadService, logService, parameterService, userService) -> preloadTimer = null rotateTimer = null @@ -44,7 +44,8 @@ cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localFora $window.Cyclotron = version: configService.version dataSources: {} - functions: {} + functions: + forceUpdate: -> $scope.$apply() parameters: _.clone $location.search() data: cyclotronDataService @@ -52,8 +53,8 @@ cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localFora currentPage = $scope.dashboard.pages[$scope.currentPageIndex] pageName = dashboardService.getPageName(currentPage, $scope.currentPageIndex) - displayName = _.jsExec($scope.dashboard.displayName) || $scope.dashboard.name - loadService.setTitle displayName + ' | ' + pageName + ' | Cyclotron' + $scope.dashboardDisplayName = _.jsExec($scope.dashboard.displayName) || $scope.dashboard.name + loadService.setTitle $scope.dashboardDisplayName + ' | ' + pageName + ' | Cyclotron' if currentPage.name? || $scope.currentPageIndex != 0 $location.path '/' + $scope.dashboard.name + '/' + _.slugify pageName else @@ -288,16 +289,6 @@ cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localFora dashboardService.setDashboardDefaults(dashboard) $scope.dashboard = dashboard - # Initialize dashboard overrides - $scope.dashboardOverrides.pages ?= [] - _.each dashboard.pages, (page, index) -> - if !$scope.dashboardOverrides.pages[index]? - $scope.dashboardOverrides.pages.push { widgets: [] } - $scope.dashboardOverrides.pages[index].widgets ?= [] - _.each page.widgets, (widget, widgetIndex) -> - if !$scope.dashboardOverrides.pages[index].widgets[widgetIndex]? - $scope.dashboardOverrides.pages[index].widgets.push {} - # Optionally disable analytics if dashboard.disableAnalytics == true configService.enableAnalytics = false @@ -406,55 +397,68 @@ cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localFora _.each themes, (theme) -> loadService.loadCssUrl('/css/app.themes.' + theme + '.css', true) - # Initialize parameters - parameterService.initializeParameters($scope.dashboard).then -> + # Initialize dashboard overrides + dashboardOverridesService.initializeDashboardOverrides($scope.dashboard).then (dashboardOverrides) -> + # Store for Dashboard use + $window.Cyclotron.dashboardOverrides = $scope.dashboardOverrides = dashboardOverrides - # Watch querystring for changes - $scope.$watch (-> $location.search()), handleQueryStringChanges + $scope.$watch 'dashboardOverrides', (updatedOverrides, previousOverrides) -> + return if _.isEqual updatedOverrides, previousOverrides + dashboardOverridesService.saveDashboardOverrides($scope.dashboard, updatedOverrides) + , true - # Watch Parameters for changes - $scope.$watch (-> $window.Cyclotron.parameters), handleParameterChanges, true - - # Preload any data sources with preload: true - preloadDataSources = _.filter $scope.dashboard.dataSources, { preload: true } + $scope.resetDashboardOverrides = -> + $window.Cyclotron.dashboardOverrides = $scope.dashboardOverrides = dashboardOverridesService.resetAndExpandOverrides $scope.dashboard - _.each preloadDataSources, (dataSourceDefinition) -> - logService.info 'Preloading data source', dataSourceDefinition.name - dataSource = dataService.get(dataSourceDefinition) - dataSource.getData(dataSourceDefinition, _.noop, _.noop, _.noop) - return + # Initialize parameters + parameterService.initializeParameters($scope.dashboard).then -> + + # Watch querystring for changes + $scope.$watch (-> $location.search()), handleQueryStringChanges + + # Watch Parameters for changes + $scope.$watch (-> $window.Cyclotron.parameters), handleParameterChanges, true + + # Preload any data sources with preload: true + preloadDataSources = _.filter $scope.dashboard.dataSources, { preload: true } - # Only load if there are any pages - if $scope.dashboard.pages.length > 0 - # Navigate to a particular page if specified - if $window.Cyclotron.parameters.page? - $scope.goToPage parseInt($window.Cyclotron.parameters.page) - else if !_.isEmpty $scope.originalDashboardPageName - pageNames = _.pluck $scope.dashboard.pages, 'name' - pageIndex = _.findIndex pageNames, (name) -> - name? and _.slugify(name) == $scope.originalDashboardPageName - - if pageIndex >= 0 - $scope.goToPage 1 + pageIndex - else if $scope.originalDashboardPageName.match(/page-\d+$/) - $scope.goToPage parseInt($scope.originalDashboardPageName.substring(5)) + _.each preloadDataSources, (dataSourceDefinition) -> + logService.info 'Preloading data source', dataSourceDefinition.name + dataSource = dataService.get(dataSourceDefinition) + dataSource.getData(dataSourceDefinition, _.noop, _.noop, _.noop) + return + + # Only load if there are any pages + if $scope.dashboard.pages.length > 0 + # Navigate to a particular page if specified + if $window.Cyclotron.parameters.page? + $scope.goToPage parseInt($window.Cyclotron.parameters.page) + else if !_.isEmpty $scope.originalDashboardPageName + pageNames = _.pluck $scope.dashboard.pages, 'name' + pageIndex = _.findIndex pageNames, (name) -> + name? and _.slugify(name) == $scope.originalDashboardPageName + + if pageIndex >= 0 + $scope.goToPage 1 + pageIndex + else if $scope.originalDashboardPageName.match(/page-\d+$/) + $scope.goToPage parseInt($scope.originalDashboardPageName.substring(5)) + else + $scope.goToPage 1 else $scope.goToPage 1 - else - $scope.goToPage 1 - $scope.updateUrl() + $scope.updateUrl() - # Start Dashboard Rotation - if $window.Cyclotron.parameters.autoRotate == "true" or $scope.dashboard.autoRotate == true - # ?autoRotate=true/false will override the dashboard setting. - # There is also a dashboard property which can disable rotation. - # Only enable rotation if there are multiple pages - if $scope.dashboard.pages.length > 1 - $scope.paused = false - $scope.rotate() - - $scope.firstLoad = false + # Start Dashboard Rotation + if $window.Cyclotron.parameters.autoRotate == "true" or $scope.dashboard.autoRotate == true + # ?autoRotate=true/false will override the dashboard setting. + # There is also a dashboard property which can disable rotation. + # Only enable rotation if there are multiple pages + if $scope.dashboard.pages.length > 1 + $scope.paused = false + $scope.rotate() + + $scope.firstLoad = false # Initial load - load dashboard and initialize rotation $scope.reloadInterval = if $window.Cyclotron.parameters.live == 'true' then 1500 else 60000 @@ -466,14 +470,8 @@ cyclotronApp.controller 'DashboardController', ($scope, $stateParams, $localFora .search $scope.deeplinkOptions .toString() - # Load Overrides, then the dashboard - $localForage.bind($scope, { - key: 'dashboardOverrides' - defaultValue: { pages: [] } - }).then -> - $window.Cyclotron.dashboardOverrides = $scope.dashboardOverrides - logService.debug 'Dashboard Overrides: ' + JSON.stringify($scope.dashboardOverrides) - $scope.loadDashboard().then $scope.initialLoad + # Load the dashboard + $scope.loadDashboard().then $scope.initialLoad # # Hot Key Bindings diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboard.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboard.coffee new file mode 100644 index 0000000..4b0bb31 --- /dev/null +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboard.coffee @@ -0,0 +1,108 @@ +### +# Copyright (c) 2013-2016 the original author or authors. +# +# Licensed under the MIT License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.opensource.org/licenses/mit-license.php +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +### + +# +# Top-level Dashboard directive +# +# Renders a series of Widgets and manages page-level interactivity. Expects the following +# scope variables: +# page: Page to render +# pageOverrides: Overrides for the current page +# pageNumber: Index of the Page in the Dashboard (zero-indexed) +# dashboard: Entire Dashboard object +# +cyclotronDirectives.directive 'dashboard', ($compile, $window, $timeout, configService, layoutService, logService) -> + { + restrict: 'AC' + + link: (scope, element, attrs) -> + $element = $(element) + $dashboardSidebar = $element.children '.dashboard-sidebar' + $dashboardControls = $element.children '.dashboard-controls' + + scope.controlTimer = null + + calculateMouseTarget = -> + # Get all dimensions and the padding options + + controlOffset = $dashboardControls.offset() + controlWidth = $dashboardControls.width() + controlHeight = $dashboardControls.height() + padX = configService.dashboard.controls.hitPaddingX + padY = configService.dashboard.controls.hitPaddingY + + return unless $dashboardControls? and controlOffset? + + scope.controlTarget = { + top: controlOffset.top - padY + bottom: controlOffset.top + controlHeight + padY + left: controlOffset.left - padX + right: controlOffset.left + controlWidth + padX + } + + makeControlsDisappear = -> + $dashboardControls.removeClass 'active' + + makeControlsAppear = _.throttle(-> + # Make visible + $dashboardControls.addClass 'active' + + # Set timer to remove the controls after some delay + $timeout.cancel(scope.controlTimer) if scope.controlTimer? + + scope.controlTimer = $timeout(makeControlsDisappear, configService.dashboard.controls.duration) + , 500, { leading: true }) + + controlHitTest = (event) -> + return unless scope.controlTarget? + # Abort if outside the target + if event.pageX < scope.controlTarget.left || + event.pageX > scope.controlTarget.right || + event.pageY < scope.controlTarget.top || + event.pageY > scope.controlTarget.bottom + return + + makeControlsAppear() + + # + # Configure Dashboard Controls + # + calculateMouseTarget() + + # + # Bind mousemove event for entire document (remove during $destroy) + # + $(document).on 'mousemove', controlHitTest + + $(document).on 'scroll', calculateMouseTarget + + $(window).on 'resize', _.debounce(-> + scope.$apply(calculateMouseTarget) + , 500, { leading: false, maxWait: 1000 }) + + # + # Cleanup + # + scope.$on '$destroy', -> + $(document).off 'mousemove', controlHitTest + $(document).off 'scroll', calculateMouseTarget + $(window).off 'resize', calculateMouseTarget + + # Cancel timer + $timeout.cancel(scope.controlTimer) if scope.controlTimer? + + return + } diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardPage.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardPage.coffee index dfe6437..57eecf5 100644 --- a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardPage.coffee +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardPage.coffee @@ -38,16 +38,16 @@ cyclotronDirectives.directive 'dashboardPage', ($compile, $window, $timeout, con template: '
' + '
' + '
' + + ' widget="widget" page="page" page-overrides="pageOverrides" widget-index="$index" layout="layout" dashboard="dashboard" post-layout="postLayout()">
' + '
' link: (scope, element, attrs) -> $element = $(element) + $dashboard = $element.parents('.dashboard') $dashboardPageInner = $element.children('.dashboard-page-inner') - $dashboardControls = $('.dashboard-controls') + $dashboardControls = $dashboard.find '.dashboard-controls' + $dashboardSidebar = $dashboard.find '.dashboard-sidebar' - scope.controlTimer = null - masonry = (element, layout) -> $dashboardPageInner.masonry({ itemSelector: '.dashboard-widgetwrapper' @@ -55,59 +55,6 @@ cyclotronDirectives.directive 'dashboardPage', ($compile, $window, $timeout, con gutter: layout.gutter }) - calculateMouseTarget = -> - # Get all dimensions and the padding options - - controlOffset = $dashboardControls.offset() - controlWidth = $dashboardControls.width() - controlHeight = $dashboardControls.height() - padX = configService.dashboard.controls.hitPaddingX - padY = configService.dashboard.controls.hitPaddingY - - return unless $dashboardControls? and controlOffset? - - scope.controlTarget = { - top: controlOffset.top - padY - bottom: controlOffset.top + controlHeight + padY - left: controlOffset.left - padX - right: controlOffset.left + controlWidth + padX - } - - makeControlsDisappear = -> - $dashboardControls.removeClass 'active' - - makeControlsAppear = _.throttle(-> - # Make visible - $dashboardControls.addClass 'active' - - # Set timer to remove the controls after some delay - $timeout.cancel(scope.controlTimer) if scope.controlTimer? - - scope.controlTimer = $timeout(makeControlsDisappear, configService.dashboard.controls.duration) - , 500, { leading: true }) - - controlHitTest = (event) -> - # Abort if outside the target - if event.pageX < scope.controlTarget.left || - event.pageX > scope.controlTarget.right || - event.pageY < scope.controlTarget.top || - event.pageY > scope.controlTarget.bottom - return - - makeControlsAppear() - - # - # Configure Dashboard Controls - # - calculateMouseTarget() - - # - # Bind mousemove event for entire document (remove during $destroy) - # - $(document).on 'mousemove', controlHitTest - - $(document).on 'scroll', calculateMouseTarget - # # Watch the dashboard page and update the layout # @@ -121,8 +68,10 @@ cyclotronDirectives.directive 'dashboardPage', ($compile, $window, $timeout, con if (newValue.enableMasonry != false) masonry(element, scope.layout) return - - scope.layout = layoutService.getLayout(newValue, $($window).width(), $($window).height()) + + containerWidth = $dashboard.innerWidth() + containerHeight = $($window).height() + scope.layout = layoutService.getLayout(newValue, containerWidth, containerHeight) # Set page margin if defined if !_.isNullOrUndefined(scope.layout.margin) @@ -139,31 +88,28 @@ cyclotronDirectives.directive 'dashboardPage', ($compile, $window, $timeout, con else $element.parents().removeClass 'fullscreen' - # Store updated hit target for the dashboard controls - calculateMouseTarget() - - # Update everything updateLayout() - resizeFunction = _.throttle(-> - scope.$apply(updateLayout) + resizeFunction = _.throttle(-> + scope.$apply updateLayout , 65) - # Update on window resizing - $(window).on 'resize', resizeFunction + # Update on element resizing + $element.on 'resize', resizeFunction scope.$on '$destroy', -> - $(window).off 'resize', resizeFunction + $element.off 'resize', resizeFunction # Apply page theme class to dashboard-controls + $dashboard.addClass('dashboard-' + newValue.theme) $dashboardControls.addClass('dashboard-' + newValue.theme) # Set dashboard background color from theme themeSettings = configService.dashboard.properties.theme.options[newValue.theme] if newValue.theme? and themeSettings? color = themeSettings.dashboardBackgroundColor - $('.dashboard, html').css('background-color', color) + $('html').css('background-color', color) return @@ -171,12 +117,6 @@ cyclotronDirectives.directive 'dashboardPage', ($compile, $window, $timeout, con # Cleanup # scope.$on '$destroy', -> - $(document).off 'mousemove', controlHitTest - $(document).off 'scroll', calculateMouseTarget - - # Cancel timer - $timeout.cancel(scope.controlTimer) if scope.controlTimer? - # Uninitialize Masonry if still present if $dashboardPageInner.data('masonry') $dashboardPageInner.masonry('destroy') diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardSidebar.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardSidebar.coffee new file mode 100644 index 0000000..a483b2b --- /dev/null +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardSidebar.coffee @@ -0,0 +1,116 @@ +### +# Copyright (c) 2013-2016 the original author or authors. +# +# Licensed under the MIT License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.opensource.org/licenses/mit-license.php +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +### + +cyclotronDirectives.directive 'dashboardSidebar', ($timeout, layoutService) -> + { + restrict: 'EAC' + link: (scope, element, attrs) -> + # Initial position + scope.sidebarExpanded = false + + $element = $(element) + $parent = $element.parent() + $header = $element.find '.sidebar-header' + $accordion = $element.find '.sidebar-accordion' + $footer = $element.find '.sidebar-footer' + $hitbox = $element.find '.sidebar-expander-hitbox' + $expander = $element.find '.sidebar-expander' + $expanderIcon = $expander.children 'i' + $clickCover = $parent.find '.click-cover' + + scope.$watch 'sidebarExpanded', (expanded) -> + if expanded + $element.removeClass 'collapsed' + $clickCover.css 'display', 'block' + $expanderIcon.removeClass 'fa-caret-right' + $expanderIcon.addClass 'fa-caret-left' + $hitbox.attr 'title', 'Click to collapse the sidebar' + else + $element.addClass 'collapsed' + $clickCover.css 'display', 'none' + $expanderIcon.removeClass 'fa-caret-left' + $expanderIcon.addClass 'fa-caret-right' + $hitbox.attr 'title', 'Click to expand the sidebar' + + $hitbox.on 'click', (event) -> + event.preventDefault() + scope.$apply -> + scope.sidebarExpanded = !scope.sidebarExpanded + + $clickCover.on 'click', (event) -> + event.preventDefault() + scope.$apply -> + scope.sidebarExpanded = false + + # Resize accordion around header/footer + sizer = -> + $accordion.height($element.outerHeight() - $header.outerHeight() - $footer.outerHeight()) + + $element.on 'resize', _.debounce(-> + scope.$apply sizer + , 300, { leading: false, maxWait: 600 }) + + # Run in 100ms + timer = $timeout sizer, 100 + + scope.$on '$destroy', -> + $timeout.cancel timer + $element.off 'resize' + + return + + controller: ($scope, configService, dashboardService) -> + $scope.footerLogos = configService.dashboardSidebar?.footer?.logos || [] + $scope.widgetVisibilities = [] + $scope.widgetOverrides = [] + + $scope.updateVisibility = -> + actualWidgets = $scope.currentPage[0]?.widgets + $scope.widgetOverrides = $scope.dashboardOverrides?.pages[$scope.currentPageIndex]?.widgets + + $scope.widgetVisibilities = _.map actualWidgets, (widget, index) -> + # Visible by default + visible = true + + if $scope.widgetOverrides?[index].hidden? + visible = !$scope.widgetOverrides?[index].hidden + else if widget.hidden + visible = false + + return { + label: dashboardService.getWidgetName(widget, index) + visible: visible + } + + $scope.changeVisibility = (widget, index) -> + + if widget.visible == true + $scope.widgetOverrides[index].hidden = false + else + $scope.widgetOverrides[index].hidden = true + return + + $scope.$watch 'currentPage', (currentPage) -> + return unless currentPage?.length > 0 + $scope.updateVisibility() + , true + + $scope.$watch 'dashboardOverrides', (dashboardOverrides) -> + return unless dashboardOverrides? + $scope.updateVisibility() + , true + + } diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardWidget.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardWidget.coffee index 898a387..071a6b6 100644 --- a/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardWidget.coffee +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.dashboardWidget.coffee @@ -32,6 +32,7 @@ cyclotronDirectives.directive 'dashboardWidget', (layoutService) -> return scope.$watch('layout', (layout) -> + return unless layout? # Set the border width if overloaded (otherwise keep theme default) if layout.borderWidth? diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.sidebarAccordion.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.sidebarAccordion.coffee new file mode 100644 index 0000000..c916fdb --- /dev/null +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.sidebarAccordion.coffee @@ -0,0 +1,144 @@ +### +# Copyright (c) 2016 the original author or authors. +# +# Licensed under the MIT License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.opensource.org/licenses/mit-license.php +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +### + +# +# Accordion for Dashboard Sidebar +# Adapted from http://jsfiddle.net/hanspc/TBz9F/ +# + +cyclotronDirectives.directive 'sidebarAccordion', ($sce, $timeout) -> + { + restrict: 'EAC' + controller: ($scope, $attrs) -> + + this.groups = [] + + $scope.trustHtml = (html) -> + $sce.trustAsHtml(html) + + # Ensure that all the groups in this accordion are closed + this.closeOthers = (openGroup) -> + angular.forEach this.groups, (group) -> + group.isOpen = false unless group == openGroup + + this.calcHeight() + + # Watch for height changes + that = this + $scope.$watch 'accordionHeight', (value) -> + that.calcHeight() + + this.calcHeight = -> + height = _.reduce this.groups, (sum, group) -> + sum + group.returnHeight() + , 0 + + that.panelHeight = $scope.getAccordionHeight() - height + + # This is called from the accordion-group directive to add itself to the accordion + this.addGroup = (groupScope) -> + that = this + this.groups.push(groupScope) + + groupScope.$on '$destroy', (event) -> + that.removeGroup(groupScope) + + # This is called from the accordion-group directive when to remove itself + this.removeGroup = (group) -> + index = this.groups.indexOf(group) + if index != -1 + this.groups.splice(index, 1) + + return + + transclude: true, + replace: false, + templateUrl: '/partials/sidebarAccordion.html' + link: (scope, element, attrs) -> + scope.getAccordionHeight = -> $(element).height() + + # Track height of accordion + sizer = -> + scope.accordionHeight = scope.getAccordionHeight() + + $(element).on 'resize', _.throttle(-> + scope.$apply sizer + , 100) + + # Run in 100ms + timer = $timeout sizer, 100 + + scope.$on '$destroy', -> + $timeout.cancel timer + $(element).off 'resize' + } + +cyclotronDirectives.directive 'accordionGroup', -> + { + require: '^sidebarAccordion' + restrict: 'EA' + transclude: true + replace: true + templateUrl: '/partials/sidebarAccordionGroup.html' + scope: + heading: '@' + isOpen: '=?' + isDisabled: '=?' + + controller: -> + this.setHeading = (element) -> + this.heading = element + + link: (scope, element, attrs, accordionController) -> + accordionController.addGroup(scope) + + scope.$watch 'isOpen', (value) -> + if value + accordionController.closeOthers(scope) + + scope.toggleOpen = -> + if !scope.isDisabled + scope.isOpen = !scope.isOpen + + scope.returnHeight = -> + element.find('.panel-heading').outerHeight(true) + + scope.$watch (-> accordionController.panelHeight), (value) -> + if value + scope.styles = + height: accordionController.panelHeight + 'px' + } + +cyclotronDirectives.directive 'accordionHeading', -> + { + restrict: 'EA' + transclude: true + template: '' + replace: true + require: '^accordionGroup' + link: (scope, element, attr, accordionGroupController, transclude) -> + accordionGroupController.setHeading(transclude(scope, -> )) + } + +cyclotronDirectives.directive 'accordionTransclude', -> + { + require: '^accordionGroup' + link: (scope, element, attr, controller) -> + scope.$watch (-> controller[attr.accordionTransclude]), (heading) -> + if heading + element.html '' + element.append heading + } diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.widget.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.widget.coffee index b53defb..06fe782 100644 --- a/cyclotron-site/app/scripts/dashboards/directives/directives.widget.coffee +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.widget.coffee @@ -34,6 +34,7 @@ cyclotronDirectives.directive 'widget', ($compile, $sce, $window, layoutService) widgetIndex: '=' layout: '=' dashboard: '=' + page: '=' pageOverrides: '=' postLayout: '&' @@ -46,69 +47,8 @@ cyclotronDirectives.directive 'widget', ($compile, $sce, $window, layoutService) scope.widgetLayout = { } - # Determine if a Widget should be visible or hidden on the dashboard - isWidgetHidden = -> - return false unless scope.widget? - - if scope.pageOverrides?.widgets? - widgetOverrides = scope.pageOverrides.widgets?[scope.widgetIndex] - - # If WidgetOverrides.hidden is set true or false, use its value - if widgetOverrides?.hidden? - return widgetOverrides.hidden == true - - # Else, default to the widget's "hidden" property - return scope.widget.hidden == true - - # Store Widget API for use by Dashboards - if scope.widget.name? - $window.Cyclotron.currentPage.widgets[scope.widget.name] = { - show: -> - scope.$apply -> - widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] - widgetOverrides.hidden = false - hide: -> - scope.$apply -> - widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] - widgetOverrides.hidden = true - toggleVisibility: -> - scope.$apply -> - widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] - widgetOverrides.hidden = !widgetOverrides.hidden - } - - # Watch for widget visibility to change - scope.$watch isWidgetHidden, (isHidden) -> - scope.layout.hidden = isHidden - - # Watch for the widget model to change, indicating this widget needs to be updated - scope.$watch 'widget', (newValue, oldValue) -> - widget = newValue - - # Ignore widgets without a type - return if _.isEmpty widget.widget - - noscrollClass = if widget.noscroll == true - ' widget-noscroll' - else - '' - - # Create the include for the specific widget referenced - template = '
' - - if widget.allowFullscreen != false - template = '' + template - - compiledValue = $compile(template)(scope) - - # Replace the current contents with the newly compiled element - element.contents().remove() - element.append(compiledValue) - - return - - # Watch for page layout changes and resize the widget - scope.$watch('layout', (layout, oldLayout) -> + # Update the layout + updateLayout = (layout) -> # Ensure a valid layout is provided if _.isUndefined(layout) @@ -122,7 +62,7 @@ cyclotronDirectives.directive 'widget', ($compile, $sce, $window, layoutService) # Copy gridWidth/width into the scope # Apply overrides if necessary (mobile devices) if layout.forceGridWidth? - if widget.gridWidth == layout.originalGridColumns + if scope.widget.gridWidth == layout.originalGridColumns scope.widgetGridWidth = layout.gridColumns else scope.widgetGridWidth = layout.forceGridWidth @@ -169,7 +109,69 @@ cyclotronDirectives.directive 'widget', ($compile, $sce, $window, layoutService) return - , true) + # Determine if a Widget should be visible or hidden on the dashboard + isWidgetHidden = -> + return false unless scope.widget? + + if scope.pageOverrides?.widgets? + widgetOverrides = scope.pageOverrides.widgets?[scope.widgetIndex] + + # If WidgetOverrides.hidden is set true or false, use its value + if widgetOverrides?.hidden? + return widgetOverrides.hidden == true + + # Else, default to the widget's "hidden" property + return scope.widget.hidden == true + + # Store Widget API for use by Dashboards + # Use scope.$evalAsync to ensure it gets digested by Angular + if scope.widget.name? + $window.Cyclotron.currentPage.widgets[scope.widget.name] = { + show: -> + scope.$evalAsync -> + widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] + widgetOverrides.hidden = false + hide: -> + scope.$evalAsync -> + widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] + widgetOverrides.hidden = true + toggleVisibility: -> + scope.$evalAsync -> + widgetOverrides = scope.pageOverrides?.widgets?[scope.widgetIndex] + widgetOverrides.hidden = !widgetOverrides.hidden + } + + # Watch for the widget model to change, indicating this widget needs to be updated + scope.$watch 'widget', (newValue, oldValue) -> + widget = newValue + + # Ignore widgets without a type + return if _.isEmpty widget.widget + + noscrollClass = if widget.noscroll == true + ' widget-noscroll' + else + '' + + # Create the include for the specific widget referenced + template = '
' + + if widget.allowFullscreen != false + template = '' + template + + compiledValue = $compile(template)(scope) + + # Replace the current contents with the newly compiled element + element.contents().remove() + element.append(compiledValue) + + return + + # Watch for page layout changes and resize the widget + scope.$watch 'layout', updateLayout, true + + # Watch for widget visibility to change + scope.$watch 'pageOverrides', (-> updateLayout(scope.layout)), true return } diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.widgetBody.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.widgetBody.coffee index d3ba1c4..80d2559 100644 --- a/cyclotron-site/app/scripts/dashboards/directives/directives.widgetBody.coffee +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.widgetBody.coffee @@ -32,10 +32,10 @@ cyclotronDirectives.directive 'widgetBody', ($timeout) -> scope.widgetLayout.widgetBodyHeight = widgetBodyHeight # Update on window resizing - $widget.add('.title, .widget-footer').on 'resize', _.throttle(-> + $widget.add('.title, .widget-footer').on 'resize', _.debounce(-> scope.$apply -> sizer() - , 65) + , 120, { leading: false, maxWait: 500 }) # Run now & again in 100ms sizer() diff --git a/cyclotron-site/app/scripts/dashboards/directives/directives.widgetError.coffee b/cyclotron-site/app/scripts/dashboards/directives/directives.widgetError.coffee index f117629..da045fa 100644 --- a/cyclotron-site/app/scripts/dashboards/directives/directives.widgetError.coffee +++ b/cyclotron-site/app/scripts/dashboards/directives/directives.widgetError.coffee @@ -53,7 +53,7 @@ cyclotronDirectives.directive 'widgetError', ($timeout) -> scope.errorMessage = scope.dataSourceErrorMessage if not _.isString(scope.errorMessage) - $scope.errorMessage = JSON.stringify scope.errorMessage + scope.errorMessage = JSON.stringify scope.errorMessage if errorMessageLength < 30 scope.shortErrorMessage = null @@ -70,7 +70,7 @@ cyclotronDirectives.directive 'widgetError', ($timeout) -> $widget.add('.title, .widget-footer').on 'resize', _.throttle(-> scope.$apply -> sizer() - , 65) + , 120, { leading: false, maxWait: 500 }) # Run now & again in 100ms sizer() diff --git a/cyclotron-site/app/scripts/dashboards/services/services.dashboardOverridesService.coffee b/cyclotron-site/app/scripts/dashboards/services/services.dashboardOverridesService.coffee new file mode 100644 index 0000000..83ccaec --- /dev/null +++ b/cyclotron-site/app/scripts/dashboards/services/services.dashboardOverridesService.coffee @@ -0,0 +1,71 @@ +### +# Copyright (c) 2016 the original author or authors. +# +# Licensed under the MIT License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.opensource.org/licenses/mit-license.php +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +### + +# +# Manages Cyclotron.dashboardOverrides +# +cyclotronServices.factory 'dashboardOverridesService', ($localForage, $q, $window, configService, logService) -> + + getLocalStorageKey = (dashboard) -> + 'dashboardOverrides.' + dashboard.name + + resetOverrides = -> + return { pages: [] } + + expandOverrides = (dashboard, dashboardOverrides) -> + dashboardOverrides.pages ?= [] + _.each dashboard.pages, (page, index) -> + if !dashboardOverrides.pages[index]? + dashboardOverrides.pages.push { widgets: [] } + dashboardOverrides.pages[index].widgets ?= [] + _.each page.widgets, (widget, widgetIndex) -> + if !dashboardOverrides.pages[index].widgets[widgetIndex]? + dashboardOverrides.pages[index].widgets.push {} + + return dashboardOverrides + + return { + + # Load Dashboard Overrides for a Dashboard + # Returns a promise after overrides have been initialized + initializeDashboardOverrides: (dashboard) -> + return $q (resolve, reject) -> + + $localForage.getItem(getLocalStorageKey(dashboard)).then (dashboardOverrides) -> + if _.isNull dashboardOverrides + dashboardOverrides = resetOverrides() + + # Pad out the overrides with empty pages/widgets + dashboardOverrides = expandOverrides dashboard, dashboardOverrides + + logService.debug 'Dashboard Overrides: ' + JSON.stringify(dashboardOverrides) + resolve dashboardOverrides + + .catch (error) -> + logService.error 'Error loading Dashboard Overrides:', error + reject error + + resetAndExpandOverrides: (dashboard) -> + dashboardOverrides = resetOverrides() + expandOverrides dashboard, dashboardOverrides + + saveDashboardOverrides: (dashboard, dashboardOverrides) -> + $localForage.setItem(getLocalStorageKey(dashboard), dashboardOverrides).then -> + logService.debug 'Saved Dashboard Overrides to localstorage!' + .catch (error) -> + logService.error 'Error saving Dashboard Overrides:', error + + } diff --git a/cyclotron-site/app/scripts/mgmt/controller.guiEditor.coffee b/cyclotron-site/app/scripts/mgmt/controller.guiEditor.coffee index 950efc2..c3ff22f 100644 --- a/cyclotron-site/app/scripts/mgmt/controller.guiEditor.coffee +++ b/cyclotron-site/app/scripts/mgmt/controller.guiEditor.coffee @@ -188,15 +188,7 @@ cyclotronApp.controller 'GuiEditorController', ($scope, $state, $stateParams, $l $scope.getPageName = dashboardService.getPageName - $scope.getWidgetName = (widget, index) -> - if !widget.widget? || widget.widget == '' - return 'Widget ' + (index + 1) - if widget?.name?.length > 0 - return _.titleCase(widget.widget) + ': ' + widget.name - else if widget.title?.length > 0 - return _.titleCase(widget.widget) + ': ' + widget.title - else - return _.titleCase(widget.widget) + $scope.getWidgetName = dashboardService.getWidgetName $scope.getParameterName = (item, index) -> if item.name?.length > 0 diff --git a/cyclotron-site/app/scripts/mgmt/directives/directives.metricsGraphics.coffee b/cyclotron-site/app/scripts/mgmt/directives/directives.metricsGraphics.coffee index ec3eab3..6a32f2e 100644 --- a/cyclotron-site/app/scripts/mgmt/directives/directives.metricsGraphics.coffee +++ b/cyclotron-site/app/scripts/mgmt/directives/directives.metricsGraphics.coffee @@ -58,9 +58,9 @@ cyclotronDirectives.directive 'metricsGraphics', -> scope.$watch 'data', (newData) -> redraw() - $element.resize(_.throttle(-> + $element.resize _.debounce -> scope.$apply -> scope.width = $element.width() redraw() - , 65)) + , 90, { leading: false, maxWait: 200 } } diff --git a/cyclotron-site/app/styles/common/main.less b/cyclotron-site/app/styles/common/main.less index e4ed0ed..325ba33 100644 --- a/cyclotron-site/app/styles/common/main.less +++ b/cyclotron-site/app/styles/common/main.less @@ -141,6 +141,10 @@ table { cursor: pointer; } +.switch { + box-sizing: content-box !important; +} + #browserError { width: 60%; max-width: 500px; diff --git a/cyclotron-site/app/styles/common/variables.less b/cyclotron-site/app/styles/common/variables.less index 6d4ae94..0c2cff8 100644 --- a/cyclotron-site/app/styles/common/variables.less +++ b/cyclotron-site/app/styles/common/variables.less @@ -53,3 +53,38 @@ border-bottom: 1px dotted #888; } } + +.unlink() { + text-decoration: none; + cursor: pointer; + border-bottom: 0; + &:hover { + color: unset; + border-bottom: 0; + } +} + +/* Dashboard Sidebar */ +@sidebar-width: 300px; +@sidebar-expander-height: 8px; +@sidebar-expander-width: 8px; +@sidebar-header-height: 100px; +@sidebar-footer-height: 67px; +@sidebar-transition-duration: 0.5s; + +@sidebar-outline-color: #ccc; +@sidebar-expander-color: black; +@sidebar-header-color: #333333; +@sidebar-header-background-color: #f0f0f0; +@sidebar-header-font-size: 1.6rem; +@sidebar-iconbar-color: #202020; +@sidebar-iconbar-background-color: #fff; +@sidebar-heading-font-size: 1.5rem; +@sidebar-heading-color: #333333; +@sidebar-heading-background-color: #e0e0e0; +@sidebar-heading-border-bottom: 1px solid white; +@sidebar-panel-color: inherit; +@sidebar-panel-background-color: white; +@sidebar-footer-color: #333333; +@sidebar-footer-background-color: #f0f0f0; + diff --git a/cyclotron-site/app/styles/dashboards/_sidebar.less b/cyclotron-site/app/styles/dashboards/_sidebar.less new file mode 100644 index 0000000..4379df5 --- /dev/null +++ b/cyclotron-site/app/styles/dashboards/_sidebar.less @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016 the original author or authors. + * + * Licensed under the MIT License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.opensource.org/licenses/mit-license.php + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +&.has-sidebar { + /* Add a special margin on the left side of the dashboard for the sidebar */ + margin-left: @sidebar-expander-width; +} + +.dashboard-sidebar { + width: @sidebar-width; + border-right: @sidebar-expander-width solid @sidebar-outline-color; + transition: margin-left @sidebar-transition-duration, border-right @sidebar-transition-duration; + + &.collapsed { + margin-left: -@sidebar-width; + } + + .sidebar-expander-hitbox { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: @sidebar-expander-width * 1.5; + margin-right: -@sidebar-expander-width * 1.5; + z-index: 9001; + cursor: pointer; + } + + .sidebar-expander { + position: absolute; + top: 50%; + left: 0; + margin-top: -@sidebar-expander-height/2; + height: @sidebar-expander-height; + width: @sidebar-expander-width; + color: @sidebar-expander-color; + line-height: @sidebar-expander-height; + text-align: center; + } + + .sidebar-header { + display: block; + width: 100%; + border-bottom: 1px solid @sidebar-outline-color; + + color: @sidebar-header-color; + background-color: @sidebar-header-background-color; + + h1 { + font-size: @sidebar-header-font-size; + margin-bottom: 0.5rem; + padding: 1rem; + } + + table.iconbar { + border-top: 1px solid @sidebar-outline-color; + width: 100%; + color: @sidebar-iconbar-color; + background-color: @sidebar-iconbar-background-color; + td { + text-align: center; + padding: 10px 15px 5px 15px; + a { + .unlink(); + color: @sidebar-iconbar-color; + } + i.fa { + font-size: 2rem; + } + } + } + } + + .sidebar-accordion { + display: block; + width: 100%; + + .panel-group { + margin-bottom: 0; + height: 100%; + } + + .panel-body { + .box-sizing(border-box); + overflow-y: auto; + } + + .panel-group .panel-heading+.panel-collapse .panel-body { + border-top: 0; + } + + .panel { + border: 0; + color: @sidebar-panel-color; + background-color: @sidebar-panel-background-color; + } + + .panel-group .panel { + margin-bottom: 0; + border-radius: 0; + } + + .panel-group .panel+.panel { + margin-top: 0; + } + + .panel-heading { + cursor: pointer; + padding: 10px 15px; + border-bottom: @sidebar-heading-border-bottom; + border-top-right-radius: 0; + border-top-left-radius: 0; + color: @sidebar-heading-color; + background-color: @sidebar-heading-background-color; + + .panel-title { + font-size: @sidebar-heading-font-size; + } + } + } + + .sidebar-footer { + .box-sizing(border-box); + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: @sidebar-footer-height; + padding: 1rem; + border-top: 1px solid @sidebar-outline-color; + color: @sidebar-footer-color; + background-color: @sidebar-footer-background-color; + + .logos { + .user-select(none); + text-align: center; + + a { + .unlink(); + } + + img { + height: 32px; + vertical-align: top; + margin-right: .4rem; + cursor: pointer; + } + } + } +} diff --git a/cyclotron-site/app/styles/dashboards/dashboard.less b/cyclotron-site/app/styles/dashboards/dashboard.less index df393af..5bcce34 100644 --- a/cyclotron-site/app/styles/dashboards/dashboard.less +++ b/cyclotron-site/app/styles/dashboards/dashboard.less @@ -41,6 +41,23 @@ margin: 0; } + .dashboard-sidebar { + /* Basic styles and initial positioning to avoid initial transition */ + /* Sidebar styling is loaded per theme */ + * { + .box-sizing(border-box); + } + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 9000; + + &.collapsed { + margin-left: -@sidebar-width * 1.2; + } + } + .dashboard-controls { .user-select(none); z-index: 999; @@ -71,6 +88,16 @@ } } + .click-cover { + position: fixed; + z-index: 8999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + } + .dashboard-pages { position: relative; } diff --git a/cyclotron-site/app/styles/themes/dark.less b/cyclotron-site/app/styles/themes/dark.less index 18610f5..144053a 100644 --- a/cyclotron-site/app/styles/themes/dark.less +++ b/cyclotron-site/app/styles/themes/dark.less @@ -29,8 +29,32 @@ @headerColor: #202020; @tableBorderColor: #ccc; - /* Dashboard Styles */ +.dashboard.dashboard-dark { + background-color: @pageBackgroundColor; + + .dashboard-page-background { + background-color: @pageBackgroundColor; + } + + @sidebar-outline-color: #666; + @sidebar-expander-color: #ddd; + @sidebar-header-color: @color; + @sidebar-header-background-color: lighten(@pageBackgroundColor, 1%); + @sidebar-iconbar-color: lighten(@color, 10%); + @sidebar-iconbar-background-color: @widgetBackgroundColor; //lighten(@pageBackgroundColor, 8%); + @sidebar-heading-color: @color; + @sidebar-heading-background-color: lighten(@pageBackgroundColor, 15%);; + @sidebar-heading-border-bottom: 1px solid @widgetBackgroundColor; + @sidebar-panel-color: @widgetBackgroundColor; + @sidebar-panel-background-color: lighten(@pageBackgroundColor, 70%); + @sidebar-footer-color: @color; + @sidebar-footer-background-color: lighten(@pageBackgroundColor, 1%); + + /* Dashboard elements */ + @import "../dashboards/_sidebar.less"; +} + .dashboard-page { /* Well style, normal only */ &.normal { diff --git a/cyclotron-site/app/styles/themes/dark2.less b/cyclotron-site/app/styles/themes/dark2.less index 6cab203..69ab395 100644 --- a/cyclotron-site/app/styles/themes/dark2.less +++ b/cyclotron-site/app/styles/themes/dark2.less @@ -31,8 +31,32 @@ @headerPadding: 5px; - /* Dashboard Styles */ +.dashboard.dashboard-dark2 { + background-color: @pageBackgroundColor; + + .dashboard-page-background { + background-color: @pageBackgroundColor; + } + + @sidebar-outline-color: #666; + @sidebar-expander-color: #ddd; + @sidebar-header-color: @color; + @sidebar-header-background-color: lighten(@pageBackgroundColor, 15%); + @sidebar-iconbar-color: lighten(@color, 10%); + @sidebar-iconbar-background-color: @widgetBackgroundColor; //lighten(@pageBackgroundColor, 8%); + @sidebar-heading-color: @color; + @sidebar-heading-background-color: lighten(@pageBackgroundColor, 15%);; + @sidebar-heading-border-bottom: 1px solid @widgetBackgroundColor; + @sidebar-panel-color: @widgetBackgroundColor; + @sidebar-panel-background-color: lighten(@pageBackgroundColor, 70%); + @sidebar-footer-color: @color; + @sidebar-footer-background-color: lighten(@pageBackgroundColor, 1%); + + /* Dashboard elements */ + @import "../dashboards/_sidebar.less"; +} + .dashboard-page { &.normal { .dashboard-dark2.dashboard-widgetwrapper > .dashboard-widget { diff --git a/cyclotron-site/app/styles/themes/gto.less b/cyclotron-site/app/styles/themes/gto.less index 2f9fbb5..34dcd57 100644 --- a/cyclotron-site/app/styles/themes/gto.less +++ b/cyclotron-site/app/styles/themes/gto.less @@ -20,7 +20,6 @@ @import "../common/variables.less"; /* Variables */ - @color: black; @pageBackgroundColor: white; @widgetBackgroundColor: white; @@ -31,6 +30,17 @@ @tableBorderColor: #555; /* Dashboard Styles */ +.dashboard.dashboard-gto { + background-color: @pageBackgroundColor; + + .dashboard-page-background { + background-color: @pageBackgroundColor; + } + + /* Dashboard elements */ + @import "../dashboards/_sidebar.less"; +} + .dashboard-page { /* Well style, normal only */ &.normal { diff --git a/cyclotron-site/app/styles/themes/light.less b/cyclotron-site/app/styles/themes/light.less index 518adc7..43a7add 100644 --- a/cyclotron-site/app/styles/themes/light.less +++ b/cyclotron-site/app/styles/themes/light.less @@ -30,6 +30,22 @@ @tableBorderColor: #555; /* Dashboard Styles */ +.dashboard.dashboard-light { + background-color: @pageBackgroundColor; + + .dashboard-page-background { + background-color: @pageBackgroundColor; + } + + /* Dashboard elements */ + @import "../dashboards/_sidebar.less"; + + .dashboard-sidebar { + .rounded(4px); + .box-shadow(1px 1px 6px 0 rgba(0, 0, 0, 0.8)); + } +} + .dashboard-page { /* Well style, normal only */ &.normal { @@ -75,6 +91,7 @@ } .dashboard-light.dashboard-widgetwrapper { + /* Import widget styles, using the theme variables */ @import "../../widgets/annotationChart/_annotationChart.less"; @import "../../widgets/chart/_chart.less"; diff --git a/cyclotron-site/app/styles/themes/lightborderless.less b/cyclotron-site/app/styles/themes/lightborderless.less index 1212aef..5c86cff 100644 --- a/cyclotron-site/app/styles/themes/lightborderless.less +++ b/cyclotron-site/app/styles/themes/lightborderless.less @@ -30,6 +30,21 @@ @tableBorderColor: #555; /* Dashboard Styles */ +.dashboard.dashboard-lightborderless { + background-color: @pageBackgroundColor; + + .dashboard-page-background { + background-color: @pageBackgroundColor; + } + + /* Dashboard elements */ + @import "../dashboards/_sidebar.less"; + + .dashboard-sidebar { + .rounded(4px); + .box-shadow(1px 1px 6px 0 rgba(0, 0, 0, 0.8)); + } +} .dashboard-page { /* Well style, normal only */ &.normal { diff --git a/cyclotron-site/app/widgets/table/_table.less b/cyclotron-site/app/widgets/table/_table.less index 2e79e98..acab483 100644 --- a/cyclotron-site/app/widgets/table/_table.less +++ b/cyclotron-site/app/widgets/table/_table.less @@ -19,7 +19,11 @@ overflow-x: auto; height: 100%; - table th, #container { + .widget-body { + overflow-y: auto; + } + + table th { border-bottom: 1px dashed #999; } diff --git a/cyclotron-site/app/widgets/table/directives.tableColumn.coffee b/cyclotron-site/app/widgets/table/directives.tableColumn.coffee index 06308bd..7073fd3 100644 --- a/cyclotron-site/app/widgets/table/directives.tableColumn.coffee +++ b/cyclotron-site/app/widgets/table/directives.tableColumn.coffee @@ -21,7 +21,7 @@ cyclotronDirectives.directive 'tableColumn', -> border = scope.column.border if border? if border.indexOf('left') >= 0 - $(element).css('border-left-width', '1px') + $(element).css 'border-left-width', '1px' if border.indexOf('right') >= 0 - $(element).css('border-right-width', '1px') + $(element).css 'border-right-width', '1px' } diff --git a/cyclotron-site/app/widgets/table/directives.tableFixedHeader.coffee b/cyclotron-site/app/widgets/table/directives.tableFixedHeader.coffee index 0709e93..666d330 100644 --- a/cyclotron-site/app/widgets/table/directives.tableFixedHeader.coffee +++ b/cyclotron-site/app/widgets/table/directives.tableFixedHeader.coffee @@ -28,8 +28,12 @@ cyclotronDirectives.directive 'tableFixedHeader', ($window, configService) -> $table = $(element) $tableHeaders = null - $parent = $table.parent() - $container = $('
').appendTo($parent) + $widgetBody = $table.parents '.widget-body' + $container = $('
').appendTo $widgetBody + + pos = + originalTop: 0 + originalLeft: $table.position().left $container.css({ 'overflow': 'hidden' @@ -38,30 +42,32 @@ cyclotronDirectives.directive 'tableFixedHeader', ($window, configService) -> 'left': $table.position().left }).hide() - pos = - originalTop: 0 - originalLeft: $table.position().left - + # Clone table for fixed header $clonedTable = $table.clone().empty() $clonedTable.css({ 'position': 'relative' - 'top': '0' + }).appendTo($container) + # Add fixed layout if there are no column groups + if scope.columnGroups.length == 0 + $clonedTable.css 'table-layout', 'fixed' + # # Handle resize events # resize = -> - $tableHeaders = $table.find('thead') - $headerRows = $tableHeaders.find('tr') - pos.originalTop = $parent.offset().top - pos.originalLeft = $table.position().left + $tableHeaders = $table.children 'thead' + $headerRows = $tableHeaders.children 'tr' + pos.originalTop = $widgetBody.position().top + pos.originalLeft = $table.offset().left - $container.css({ - width: $parent[0].clientWidth + $container.css { + width: $widgetBody[0].clientWidth height: $tableHeaders.height() - }) + top: pos.originalTop + } $clonedTable .empty() @@ -72,23 +78,27 @@ cyclotronDirectives.directive 'tableFixedHeader', ($window, configService) -> $(this).css('height', height) $(this).find('th').each (thIndex) -> originalHeader = $headerRows.eq(index).find('th').eq(thIndex) - $(this).css('width', originalHeader.width()) + $(this).css { + width: originalHeader.width() + } - $parent.on 'resize', _.throttle(resize, 200, { leading: false, trailing: true }) + $widgetBody.on 'resize', _.throttle(resize, 250, { leading: false, maxWait: 500 }) scope.$watch 'sortBy+sortedRows', _.throttle(resize, 200, { leading: false, trailing: true }) # # Handle scroll events # - $parent.on 'scroll', -> - scrollTop = $parent.scrollTop() + $widgetBody.on 'scroll', _.debounce(-> + scrollTop = $widgetBody.scrollTop() elementTop = $tableHeaders.offset().top diff = pos.originalTop - elementTop - if (diff > 0 && scrollTop > diff && scrollTop <= (diff + $table.height() - $tableHeaders.height())) + $container.css 'top', pos.originalTop + + if scrollTop > 0 $clonedTable.css({ - 'left': -$parent.scrollLeft() + 'left': -$widgetBody.scrollLeft() }) if not scope.visible @@ -98,6 +108,7 @@ cyclotronDirectives.directive 'tableFixedHeader', ($window, configService) -> else $container.hide() scope.visible = false + , 120, { leading: false, maxWait: 200 }) # # Cleanup @@ -107,8 +118,8 @@ cyclotronDirectives.directive 'tableFixedHeader', ($window, configService) -> $container.remove() $container = null - $parent.off 'resize' - $parent.off 'scroll' + $widgetBody.off 'resize' + $widgetBody.off 'scroll' return return diff --git a/cyclotron-site/bower.json b/cyclotron-site/bower.json index fc01db9..1cc1b77 100644 --- a/cyclotron-site/bower.json +++ b/cyclotron-site/bower.json @@ -30,6 +30,7 @@ "angular-ui-ace": "0.2.3", "angular-ui-router": "0.2.14", "angular-ui-select": "0.12.1", + "angular-ui-switch": "~0.1.1", "bowser": "1.0.0", "c3": "0.4.10", "d3": "3.5.6", @@ -45,7 +46,7 @@ "lodash": "2.4.1", "masonry": "3.1.5", "metrics-graphics": "2.6.0", - "moment": "2.8.4", + "moment": "2.13.0", "ngTranscludeMod": "izhaki/ngTranscludeMod", "node-uuid": "1.4.3", "numeraljs": "1.5.3", @@ -157,5 +158,8 @@ "src/URI.min.js" ] } + }, + "devDependencies": { + "angular-ui-switch": "~0.1.1" } } diff --git a/cyclotron-site/gulpfile.coffee b/cyclotron-site/gulpfile.coffee index 9f9a4dd..8c07ab4 100644 --- a/cyclotron-site/gulpfile.coffee +++ b/cyclotron-site/gulpfile.coffee @@ -70,6 +70,10 @@ ngAnnotateOptions = remove: true single_quotes: true +plumberError = (error) -> + console.log(error) + this.emit('end') + gulp.task 'clean', (done) -> del(['./_public', './coverage', 'bower_components'], done) @@ -222,25 +226,25 @@ gulp.task 'styles', -> lessFilter = filter '**/*.less' appCommon = gulp.src './app/styles/common/*.less' - .pipe plumber() + .pipe plumber(plumberError) .pipe less() .pipe concat 'css/app.common.css' .pipe gulp.dest './_public' - appDashboards = gulp.src './app/styles/dashboards/*.less' - .pipe plumber() + appDashboards = gulp.src ['./app/styles/dashboards/*.less', '!./app/styles/dashboards/_*.less'] + .pipe plumber(plumberError) .pipe less() .pipe concat 'css/app.dashboards.css' .pipe gulp.dest './_public' appMgmt = gulp.src './app/styles/mgmt/*.less' - .pipe plumber() + .pipe plumber(plumberError) .pipe less() .pipe concat 'css/app.mgmt.css' .pipe gulp.dest './_public' themes = gulp.src './app/styles/themes/*.less' - .pipe plumber() + .pipe plumber(plumberError) .pipe less() .pipe rename { prefix: 'app.themes.' diff --git a/cyclotron-site/package.json b/cyclotron-site/package.json index ee8cfda..15b0423 100644 --- a/cyclotron-site/package.json +++ b/cyclotron-site/package.json @@ -1,7 +1,7 @@ { "name": "cyclotron-site", "description": "Cyclotron: website", - "version": "1.30.0", + "version": "1.34.0", "author": "Dave Bauman ", "license": "MIT", "private": true, diff --git a/cyclotron-site/test/unit/mixins-spec.coffee b/cyclotron-site/test/unit/mixins-spec.coffee index 37ad819..477c9c7 100644 --- a/cyclotron-site/test/unit/mixins-spec.coffee +++ b/cyclotron-site/test/unit/mixins-spec.coffee @@ -162,6 +162,9 @@ describe 'Unit: _.numeralformat', -> it 'should apply format correctly to strings', -> expect(_.numeralformat('0,0.0', '2002.041')).toBe '2,002.0' + it 'should return the string if a non-numeric string is provided', -> + expect(_.numeralformat('0,0.0', 'null')).toBe 'null' + describe 'Unit: _.ngApply', -> actual = null mockScope = { diff --git a/cyclotron-site/test/unit/mixins-varsub-spec.coffee b/cyclotron-site/test/unit/mixins-varsub-spec.coffee index 06aafc9..5274115 100644 --- a/cyclotron-site/test/unit/mixins-varsub-spec.coffee +++ b/cyclotron-site/test/unit/mixins-varsub-spec.coffee @@ -74,3 +74,6 @@ describe 'Unit: _.varSub', -> it 'should return an unchanged string if the variable name is not found but there is a format code', -> expect(_.varSub('#{number|0.0 %}', {})).toBe '#{number|0.0 %}' + + it 'should return an unchanged string if a format string is provided but the value is not a number', -> + expect(_.varSub('#{number|0.0 %}', {number: 'null'})).toBe 'null' diff --git a/cyclotron-svc/package.json b/cyclotron-svc/package.json index 81a2148..6eef149 100644 --- a/cyclotron-svc/package.json +++ b/cyclotron-svc/package.json @@ -1,7 +1,7 @@ { "name": "cyclotron-svc", "description": "Cyclotron: REST API", - "version": "1.30.0", + "version": "1.34.0", "author": "Dave Bauman ", "license": "MIT", "private": true,