diff --git a/Changelog.md b/Changelog.md index d40085c6..3be8bdaf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,10 +5,14 @@ #### Adds - Serve glyphicons in the developer workspace - [IMPAC-613] Adds custom legend icons for Cash Projection & Cash Balance widgets +- [IMPAC-622] Add threshold tooltip when triggered +- [IMPAC-693] Add currency conversion to KPI targets #### Fixes - Update doc with v1.6.0 changes - [IMPAC-658] Fix dependency to xeditable +- Fix chart-threshold panel action btns outline glitch on loading +- Improve success/error toastrs for widget kpis #### Config changes - New image asset files via the assets.svc @@ -21,6 +25,10 @@ plotLineLegendIcon: ':default/plot-line-icon.svg' areaLegendIcon: ':default/area-icon.svg' ``` +### Dependencies +- Impac! v1.6.1 (kpis#show endpoint) +- Impac! Finance Bolt v0.4.0 (kpis#show endpoint & render method) + ------------------------------------------------------------- ### v1.6.0 | 2017 - Week 33 diff --git a/src/components/dashboard-settings/currency/currency.directive.coffee b/src/components/dashboard-settings/currency/currency.directive.coffee index b1ece7b5..add20e36 100644 --- a/src/components/dashboard-settings/currency/currency.directive.coffee +++ b/src/components/dashboard-settings/currency/currency.directive.coffee @@ -29,8 +29,10 @@ module.directive('dashboardSettingCurrency', ($templateCache, $log, ImpacMainSvc ImpacDashboardsSvc.update(scope.currentDhb.id, data).then( -> scope.data.savedCurrency = scope.data.currency - ImpacWidgetsSvc.massAssignAll(data) - ImpacKpisSvc.massAssignAll(data) + ImpacKpisSvc.massAssignAll(data).finally( + -> + ImpacWidgetsSvc.massAssignAll(data) + ) -> toastr.error("Unable to select currency '#{scope.data.currency}'", 'Error') scope.data.currency = scope.data.savedCurrency diff --git a/src/components/kpi/kpi.directive.coffee b/src/components/kpi/kpi.directive.coffee index 2234a7ac..93b73a81 100644 --- a/src/components/kpi/kpi.directive.coffee +++ b/src/components/kpi/kpi.directive.coffee @@ -1,6 +1,6 @@ angular .module('impac.components.kpi', []) - .directive('impacKpi', ($log, $timeout, $templateCache, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS, $translate) -> + .directive('impacKpi', ($log, $timeout, $templateCache, $translate, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS, MNO_CURRENCIES) -> return { restrict: 'EA' scope: { @@ -15,49 +15,27 @@ angular # Private Methods # ------------------------- fetchKpiData = -> - ImpacKpisSvc.show($scope.kpi).then((renderedKpi)-> - angular.extend $scope.kpi, renderedKpi - # Extra Params - # Get the corresponding template of the KPI loaded - kpiTemplate = ImpacKpisSvc.getKpiTemplate($scope.kpi.endpoint, $scope.kpi.element_watched) - # Set the kpi name from the template - $scope.kpi.name = kpiTemplate? && kpiTemplate.name - # If the template contains extra params we add it to the KPI - if kpiTemplate? && kpiTemplate.extra_params? - $scope.kpi.possibleExtraParams = kpiTemplate.extra_params - # Init the extra params select boxes with the first param - _.forIn($scope.kpi.possibleExtraParams, (paramValues, param)-> - ($scope.kpi.extra_params ||= {})[param] = paramValues[0].id if paramValues[0] - ) - - # Targets - watchablesWithoutTargets = false - _.forEach($scope.kpi.watchables, (watchable)-> - # No targets found - initialise a target form model for watchable - if _.isEmpty (existingTargets = $scope.getTargets(watchable)) - $scope.addTargetToWatchable(watchable) - watchablesWithoutTargets = true - - # Targets found - bind existing targets to the form model - else - $scope.targets[watchable] = angular.copy(existingTargets) - ) - # All watchables must have at least one target. - $scope.displayEditSettings() if watchablesWithoutTargets + ImpacKpisSvc.show($scope.kpi).then( + (kpiData)-> + ImpacKpisSvc.applyFetchedDataToDhbKpi($scope.kpi, kpiData) + initTargetsForm(true) ) - onUpdateSettingsCb = (force)-> $scope.updateSettings() if $scope.kpi.isEditing || force + onUpdateSettingsCb = (force)-> + $scope.updateSettings() if $scope.kpi.isEditing || force - onToggleSettingsCb = -> animateKpiPanels() + onToggleSettingsCb = -> + initTargetsForm() + animateKpiPanels() - onUpdateDatesCb = -> fetchKpiData() unless $scope.kpi.static + onUpdateDatesCb = -> + fetchKpiData() unless $scope.kpi.static applyPlaceholderValues = -> - _.forEach($scope.kpi.watchables, (watchable)-> + _.each $scope.kpi.watchables, (watchable)-> data = $scope.getTargetPlaceholder(watchable) (target = {})[data.mode] = data.value $scope.targets[watchable] = [target] - ) $scope.updateSettings(true) animateKpiPanels = ()-> @@ -70,6 +48,27 @@ angular element.animate({opacity: 1}, 150) , 200 + initTargetsForm = (toggleKpiIsEditing = false)-> + if _.isEmpty($scope.kpi.targets) + _.each $scope.kpi.watchables, (watchable)-> + (newTarget = {})[$scope.getTargetPlaceholder(watchable).mode] = '' + ($scope.targets[watchable] ||= []).push(newTarget) + displayEditSettings() if toggleKpiIsEditing + else + $scope.targets = angular.copy($scope.kpi.targets) + + displayEditSettings = -> + $scope.kpi.isEditing = true + + hideEditSettings = -> + $scope.kpi.isEditing = false + + hasContent = -> + !!($scope.kpi && $scope.kpi.layout && $scope.kpi.data) + + hasValidTargets = -> + ImpacKpisSvc.validateKpiTargets($scope.targets) + # Load # ------------------------- $scope.kpiTemplates = ImpacKpisSvc.getKpisTemplates() @@ -100,28 +99,12 @@ angular # Linked methods # ------------------------- - $scope.addTargetToWatchable = (watchable)-> - return if _.has($scope.targets, watchable) - (newTarget = {})[$scope.getTargetPlaceholder(watchable).mode] = '' - ($scope.targets[watchable] ||= []).push(newTarget) - - $scope.displayEditSettings = -> - $scope.kpi.isEditing = true - - $scope.hideEditSettings = -> - $scope.kpi.isEditing = false - - $scope.hasValidTargets = -> - ImpacKpisSvc.validateKpiTargets($scope.targets) - - $scope.hasContent = -> - !!($scope.kpi && $scope.kpi.layout && $scope.kpi.data) $scope.showKpiContent = -> - !$scope.isLoading() && $scope.hasContent() + !$scope.isLoading() && hasContent() $scope.isDataNotFound = -> - !$scope.hasContent() + !hasContent() $scope.isLoading = -> $scope.kpi.isLoading @@ -130,20 +113,27 @@ angular $scope.updateSettings(true) $scope.updateSettings = (force)-> - params = {} + params = { targets: {} } touched = (form = $scope["kpi#{$scope.kpi.id}SettingsForm"]) && form.$dirty - hasValidTargets = $scope.hasValidTargets() - return $scope.cancelUpdateSettings(hasValidTargets) unless touched && hasValidTargets || force + return $scope.cancelUpdateSettings(hasValidTargets()) unless touched && hasValidTargets() || force - params.targets = $scope.targets + # Apply targets to params, adding dashboard currency as target base currency + _.each($scope.targets, (targets, watchable)-> + curr = ImpacKpisSvc.getCurrentDashboard().currency + params.targets[watchable] = _.map(targets, (t)-> angular.merge(t, currency: curr)) + ) params.extra_params = $scope.kpi.extra_params unless _.isEmpty($scope.kpi.extra_params) - ImpacKpisSvc.update($scope.kpi, params) unless _.isEmpty(params) + unless _.isEmpty(params) + ImpacKpisSvc.update($scope.kpi, params).then( + (kpiData)-> + ImpacKpisSvc.applyFetchedDataToDhbKpi($scope.kpi, kpiData) + ) form.$setPristine() # smoother update transition $timeout -> - $scope.hideEditSettings() + hideEditSettings() , 200 $scope.cancelUpdateSettings = (hasValidTargets)-> @@ -155,7 +145,7 @@ angular $scope.targets = angular.copy($scope.kpi.targets) # smoother delete transition $timeout -> - $scope.hideEditSettings() + hideEditSettings() , 200 $scope.deleteKpi = -> @@ -163,30 +153,23 @@ angular $scope.kpi.isLoading = true ImpacKpisSvc.delete($scope.kpi).then((success) -> $scope.onDelete()).finally(-> $scope.kpi.isLoading = false) - $scope.isTriggered = -> - $scope.kpi.layout? && $scope.kpi.layout.triggered - $scope.isEditing = -> $scope.kpi.isEditing || $scope.editMode $scope.getFormTargetValueInput = (watchable, targetIndex)-> $scope["kpi#{$scope.kpi.id}SettingsForm"]["#{watchable}TargetValue#{targetIndex}"] - $scope.getTargets = (watchable)-> - ($scope.kpi.targets? && $scope.kpi.targets[watchable]) || [] - $scope.getTargetUnit = (watchable)-> unit = ($scope.kpi.data? && $scope.kpi.data[watchable].unit) || $scope.getTargetPlaceholder(watchable).unit || '' - if unit == 'currency' then ImpacKpisSvc.getCurrentDashboard().currency else unit + if MNO_CURRENCIES[unit]? then ImpacKpisSvc.getCurrentDashboard().currency else unit $scope.getTargetPlaceholder = (watchable)-> ImpacKpisSvc.getKpiTargetPlaceholder($scope.kpi.endpoint, watchable) $scope.getRealValue = -> - kpi = $scope.kpi - return "" if _.isEmpty(kpi.data) - value = kpi.data[kpi.watchables[0]].value - unit = kpi.data[kpi.watchables[0]].unit + return "" if _.isEmpty($scope.kpi.data) || _.isEmpty($scope.kpi.watchables) + value = $scope.kpi.data[$scope.kpi.watchables[0]].value + unit = $scope.kpi.data[$scope.kpi.watchables[0]].unit [value, unit].join(' ').trim() # Add / remove placeholder for impac-material nice-ness. diff --git a/src/components/kpis-bar/kpis-bar.directive.coffee b/src/components/kpis-bar/kpis-bar.directive.coffee index 51829778..e81e23fc 100644 --- a/src/components/kpis-bar/kpis-bar.directive.coffee +++ b/src/components/kpis-bar/kpis-bar.directive.coffee @@ -114,7 +114,7 @@ angular opts = {} opts.extra_watchables = _.filter(kpi.watchables, (w)-> w != kpi.element_watched) - ImpacKpisSvc.create(kpi.source || 'impac', kpi.endpoint, kpi.element_watched, opts).then( + ImpacKpisSvc.create(kpi, opts).then( (success) -> $scope.kpis.push(success) (error) -> diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index dc369c20..6adaae2f 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -27,14 +27,19 @@ module.component('chartThreshold', { ctrl.loading = false ctrl.draftTarget = value: '' ctrl.chartShrinkSize ||= 38 - ctrl.disabled ||= false + # Disable ability to attach widget kpi unless a bolt widget + ctrl.disabled ||= isCmpDisabled() ctrl.kpiTargetMode ||= 'min' ctrl.kpiCreateLabel ||= 'Get alerted when the target threshold goes below' # Get attachable kpi templates ImpacKpisSvc.getAttachableKpis(ctrl.widget.endpoint).then( (templates)-> return disableAttachability('No valid KPI Templates found') if _.isEmpty(templates) || _.isEmpty(templates[0].watchables) + # Widgets can have multiple possible attachable KPIs, only one is currently supported. angular.extend(ctrl.kpi, angular.copy(templates[0])) + # The watchables are currently not selectable by the user, only one element_watched + # is supported. + ctrl.kpi.element_watched = ctrl.kpi.watchables[0] -> disableAttachability() ) @@ -69,12 +74,16 @@ module.component('chartThreshold', { return if ctrl.loading ctrl.loading = true params = targets: {}, metadata: {} - params.targets[ctrl.kpi.watchables[0]] = [{ - "#{ctrl.kpiTargetMode}": ctrl.draftTarget.value + params.targets[ctrl.kpi.element_watched] = [{ + "#{ctrl.kpiTargetMode}": ctrl.draftTarget.value, + currency: ImpacKpisSvc.getCurrentDashboard().currency }] return unless ImpacKpisSvc.validateKpiTargets(params.targets) promise = if ctrl.isEditingKpi - ImpacKpisSvc.update(getKpi(), params, false) + ImpacKpisSvc.update(getKpi(), params, false).then( + (kpi)-> + angular.extend(getKpi(), kpi) + ) else # TODO: improve the way the hist_params are applied onto widget kpis if ctrl.widget.metadata && (widgetHistParams = ctrl.widget.metadata.hist_parameters) @@ -82,13 +91,23 @@ module.component('chartThreshold', { else params.metadata.hist_parameters = ImpacUtilities.yearDates() params.widget_id = ctrl.widget.id - ImpacKpisSvc.create('impac', ctrl.kpi.endpoint, ctrl.kpi.watchables[0], params) + ImpacKpisSvc.create(ctrl.kpi, params).then( + (kpi)-> + ctrl.widget.kpis.push(kpi) + kpi + ) promise.then( (kpi)-> - ctrl.widget.kpis.push(kpi) - ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) - (err)-> - toastr.error('Failed to save KPI', 'Error') + ImpacKpisSvc.show(kpi).then( + (kpiData)-> + dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) + angular.extend(kpi, kpiData[dataKey]) + ).finally( + -> + ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) + ) + -> + toastr.error("Failed to save #{ctrl.kpi.element_watched} KPI", getWidgetName()) ).finally(-> ctrl.cancelCreateKpi() ) @@ -96,14 +115,13 @@ module.component('chartThreshold', { ctrl.deleteKpi = -> return if ctrl.loading ctrl.loading = true - kpiDesc = "#{ctrl.widget.name} #{(kpi = getKpi()).element_watched}" - ImpacKpisSvc.delete(kpi).then( + ImpacKpisSvc.delete(getKpi()).then( -> - toastr.success("Deleted #{kpiDesc} KPI") - _.remove(ctrl.widget.kpis, (k)-> k.id == kpi.id) + toastr.success("Deleted #{ctrl.kpi.element_watched} KPI", getWidgetName()) + _.remove(ctrl.widget.kpis, (k)-> k.id == getKpi().id) ctrl.onComplete($event: {}) if _.isFunction(ctrl.onComplete) -> - toastr.error("Failed to delete #{kpiDesc} KPI", 'Error') + toastr.error("Failed to delete #{ctrl.kpi.element_watched} KPI", getWidgetName()) ).finally(-> ctrl.cancelCreateKpi() ) @@ -113,6 +131,9 @@ module.component('chartThreshold', { getKpi = -> _.find(ctrl.widget.kpis, (k)-> k.id == ctrl.draftTarget.kpiId) + getWidgetName = -> + _.startCase "#{ctrl.widget.name} widget" + onChartNotify = (chart)-> ctrl.chart = chart validateHistParameters() @@ -139,8 +160,8 @@ module.component('chartThreshold', { disableAttachability = (logMsg)-> ctrl.disabled = true - toastr.warning("Chart threshold KPI disabled!", "#{ctrl.widget.name} Widget") - $log.warn("Impac! - #{ctrl.widget.name} Widget: #{logMsg}") if logMsg + toastr.warning('Chart KPIs are disabled!', getWidgetName()) + $log.warn("Impac! - #{getWidgetName()}: #{logMsg}") if logMsg # As this method can be called from parent component or an event callback, # $timeout to ensure value change is detected as per usual. @@ -168,5 +189,12 @@ module.component('chartThreshold', { ctrl.disabled = _.isEmpty(widgetHistParams) || moment(widgetHistParams.to) <= moment().startOf('day') return + isCmpDisabled = -> + if _.isEmpty(ctrl.widget.metadata.bolt_path) + $log.error("chart-threshold.component not compatible with #{getWidgetName()} - no bolt path defined") + true + else + false + return ctrl }) diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.less b/src/components/widgets-common/chart-threshold/chart-threshold.less index 728f4ec6..7d3d3a1b 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.less +++ b/src/components/widgets-common/chart-threshold/chart-threshold.less @@ -37,10 +37,9 @@ chart-threshold { button { padding: 3px 6px; display: inline-block; - } - - & > .btn.has-spinner { - i { padding: 0 15px; } + &.loading { + padding: 3px 20px; + } } } } diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html b/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html index 52bdff9b..d1fa0c75 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html +++ b/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html @@ -6,11 +6,11 @@