diff --git a/Changelog.md b/Changelog.md index a612d761..497a82cf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ ### v1.6.2 | 2017 - Week 39 #### Adds +- [IMPAC-693] Add currency conversion to KPI targets #### Fixes 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 7a785670..3e61b85f 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -36,7 +36,11 @@ module.component('chartThreshold', { 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() ) @@ -71,28 +75,42 @@ module.component('chartThreshold', { return if ctrl.loading ctrl.loading = true params = targets: {}, metadata: {} - params.targets[ctrl.kpi.watchables[0]] = [{ + params.targets[ctrl.kpi.element_watched] = [{ "#{ctrl.kpiTargetMode}": parseFloat(ctrl.draftTarget.value) + currency: ImpacKpisSvc.getCurrentDashboard().currency }] return unless ImpacKpisSvc.validateKpiTargets(params.targets) promise = if ctrl.isEditingKpi - ImpacKpisSvc.update(getKpi(), params, false).then( - (kpi)-> + kpi = getKpi() + ImpacKpisSvc.update(kpi, params, false).then( + (updatedKpi)-> # Remove old threshold from chart ctrl.chart.removeThreshold(kpi.id) - angular.extend(getKpi(), kpi) + angular.extend(kpi, updatedKpi) ) else - params.metadata.hist_parameters = ctrl.widget.metadata.hist_parameters + params.metadata.hist_parameters = { + from: moment.utc().format('YYYY-MM-DD') + to: moment.utc(getChartExtremes().xAxis.max).format('YYYY-MM-DD') + } params.widget_id = ctrl.widget.id - ImpacKpisSvc.create('impac', ctrl.kpi.endpoint, ctrl.kpi.watchables[0], params).then( + ImpacKpisSvc.create(ctrl.kpi, params).then( (kpi)-> ctrl.widget.kpis.push(kpi) kpi ) promise.then( (kpi)-> - ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) + 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() ) @@ -100,15 +118,15 @@ module.component('chartThreshold', { ctrl.deleteKpi = -> return if ctrl.loading ctrl.loading = true - kpiDesc = "#{ctrl.widget.name} #{(kpi = getKpi()).element_watched}" + kpi = getKpi() ImpacKpisSvc.delete(kpi).then( -> - toastr.success("Deleted #{kpiDesc} KPI") + toastr.success("Deleted #{ctrl.kpi.element_watched} KPI", getWidgetName()) _.remove(ctrl.widget.kpis, (k)-> k.id == kpi.id) ctrl.chart.removeThreshold(kpi.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() ) @@ -118,17 +136,19 @@ 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 - return unless validateHistParameters() Highcharts.addEvent(chart.hc.container, 'click', onChartClick) _.each buildThresholdsFromKpis(), (threshold)-> - thresholdSerie = ctrl.chart.findThreshold(threshold.kpiId) - thresholdSerie = ctrl.chart.addThreshold(threshold) unless thresholdSerie? + thresholdSerie = ctrl.chart.updateThreshold(threshold) ctrl.chart.addThresholdEvent(thresholdSerie, 'click', onThresholdClick) return onChartClick = (event)-> + return unless hasFutureChartMaxDate() # Check whether click event fired is from the 'reset zoom' button return if event.srcElement.textContent == 'Reset zoom' # Guard for tooltips / other chart areas that don't return a yAxis value @@ -144,8 +164,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. @@ -165,17 +185,34 @@ module.component('chartThreshold', { ctrl.chart.hc.setSize(null, ctrl.chart.hc.chartHeight + ctrl.chartShrinkSize, false) ctrl.chart.hc.container.parentElement.style.height = "#{ctrl.chart.hc.chartHeight}px" - # Disable threshold when selected time period is strictly in the past - validateHistParameters = -> - widgetHistParams = ctrl.widget.metadata && ctrl.widget.metadata.hist_parameters - ctrl.disabled = widgetHistParams? && moment(widgetHistParams.to) <= moment.utc().startOf('day') - return !ctrl.disabled + hasFutureChartMaxDate = -> + return false unless ctrl.chart && ctrl.chart.hc + moment.utc(getChartExtremes().xAxis.max) > moment() + + getChartExtremes = -> + xAxis: ctrl.chart.hc.xAxis[0].getExtremes() - # Validate and build threshold data from widget kpi templates + # No support for multiple KPIs & watchables yet. buildThresholdsFromKpis = -> - targets = ctrl.widget.kpis? && ctrl.widget.kpis[0] && ctrl.widget.kpis[0].targets - return [] unless ImpacKpisSvc.validateKpiTargets(targets) - [{ kpiId: ctrl.widget.kpis[0].id, value: targets.threshold[0].min, name: 'Alert Threshold', color: ctrl.thresholdColor }] + return unless (kpi = ctrl.widget.kpis && ctrl.widget.kpis[0]) && + (watchable = kpi.watchables && kpi.watchables[0]) && + (targets = watchable && watchable.targets) + _.map(targets, (t)-> + name: 'Alert Threshold' + kpiId: kpi.id + value: t.min + triggered: t.trigger_state + triggered_interval_index: t.triggered_interval_index + color: ctrl.thresholdColor + ) + + + 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-settings/attach-kpis/attach-kpis.directive.coffee b/src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee deleted file mode 100644 index a7f80a0e..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee +++ /dev/null @@ -1,136 +0,0 @@ -### -# Attach KPIs onto widget with a form for picking target mode and value. View widget's -# attached KPIs, manage set targets, alerts and delete. -# **NOTE: this component is not in use, and requires fixes/improvements to be used.** -### -module = angular.module('impac.components.widgets-settings.attach-kpis', []) -module.directive('settingAttachKpis', ($templateCache, ImpacWidgetsSvc, ImpacKpisSvc, $translate)-> - - return { - restrict: 'A' - scope: { - parentWidget: '=' - attachedKpis: '=?' - widgetEngine: '=' - widgetId: '=' - extraParams: '=?' - deferred: '=' - showExtraParam: '=?' - } - template: $templateCache.get('widgets-settings/attach-kpis.tmpl.html') - - controller: ($scope)-> - w = $scope.parentWidget - - # Settings configurations - # ----------------------- - - settings = {} - - settings.initialize = -> - loadKpisData() - - settings.toMetadata = -> - - w.settings.push(settings) - - # Linked methods - # ----------------------- - - $scope.formatKpiName = (endpoint)-> - ImpacKpisSvc.formatKpiName(endpoint) - - $scope.hasValidTarget = -> - ImpacKpisSvc.validateKpiTarget($scope.kpi) - - $scope.attachKpi = -> - params = {} - return unless $scope.hasValidTarget() - - target0 = {} - target0[$scope.kpi.limit.mode] = $scope.kpi.limit.value - - params.targets = {} - params.targets[$scope.kpi.watchables[0]] = [target0] - params.widget_id = $scope.widgetId - - # NOTE: When multiple extra param functionality is added, this should be - # more dynamic via a selection ngModel or similar. - for param, paramValues of $scope.extraParams - params.extra_params ||= {} - params.extra_params[param] = paramValues.uid - - console.log('attachKpis: ', $scope.kpi.endpoint, $scope.elementWatched, params) - - ImpacKpisSvc.create('impac', $scope.kpi.endpoint, $scope.elementWatched, params).then( - (kpi)-> - console.log('attached KPI: ', kpi) - $scope.attachedKpis.push(kpi) - # ImpacKpisSvc.show(kpi).then(-> - # # TODO: display interesting things (e.g graph overlays) with KPI data! - # ) - ) - - $scope.deleteKpi = (kpi)-> - ImpacKpisSvc.delete(kpi, {widget_id: $scope.widgetId}).then(-> - _.remove($scope.attachedKpis, (k)-> k.id == kpi.id ) - ) - - # Builds formatted kpi titles for attached kpis based on the set targets, - # possibleTargets mappings, and the kpi.data.unit returned from impac!. - # --- - # NOTE: if multiple targets are to be supported, this should be revised. - $scope.formatAttachedKpiTitle = (kpi)-> - return '' unless kpi.data && kpi.targets && $scope.elementWatched - ImpacKpisSvc.formatKpiTarget(kpi.targets[$scope.elementWatched][0], kpi.data[$scope.elementWatched].unit, $scope.possibleTargets) - - # Local methods - # ----------------------- - - - # On-load - # ----------------------- - - $scope.attachedKpis ||= [] - - # Mapping target modes to labels. - $scope.possibleTargets = [ - { label: $translate.instant('impac.widget.settings.attach_kpis.over'), mode: 'min' } - { label: $translate.instant('impac.widget.settings.attach_kpis.below'), mode: 'max' } - ] - - # Prepare Attachable KPI model. - $scope.kpi = { - limit: { mode: $scope.possibleTargets[0].mode } - # possibleExtraParams: $scope.extraParams - } - - # Load Attachable KPI Templates. - # ------------------------------------- - ImpacKpisSvc.getAttachableKpis($scope.widgetEngine).then((kpiTemplates)-> - $scope.availableKpis = angular.copy(kpiTemplates) - # Set default kpi. - # TODO: support for multiple kpi's. - angular.extend($scope.kpi, $scope.availableKpis[0]) - # Set default extra param. - # TODO: support for multiple extra params. - $scope.selectedParam = _.keys($scope.extraParams)[0] - # TODO: support for watchable selection. - $scope.elementWatched = $scope.kpi.watchables? && $scope.kpi.watchables[0] - ) - - # Load attached KPI's data. - loadKpisData = -> - # _.forEach($scope.attachedKpis, (kpi)->) - # ImpacKpisSvc.show(kpi).then((res)-> - # # TODO: display interesting things (e.g graph overlays) with KPI data! - # ) - # ) - - loadKpisData() - - # Setting is ready: trigger load content - # ------------------------------------ - $scope.deferred.resolve($scope.parentWidget) - } -) diff --git a/src/components/widgets-settings/attach-kpis/attach-kpis.less b/src/components/widgets-settings/attach-kpis/attach-kpis.less deleted file mode 100644 index ff5f2ba3..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.less +++ /dev/null @@ -1,98 +0,0 @@ -.analytics .settings.attach-kpis { - .attach-kpi { - padding: 8px 5px; - - form .row { - padding-bottom: 5px; - } - - .attach-kpi-form { - background-color: white; - border: 1px solid #ddd; - padding: 5px; - } - - form .row.kpi-description { - padding: 10px 2px; - span { - display: block; - font-weight: bold; - font-size: 13px; - } - } - - form.attach-kpi-form { - input.attach-target { - background-color: white; - height: inherit; - } - } - - .error-messages { - color: @brand-danger; - width: 120px; - } - } - - .attached-kpis { - padding: 5px; - } - - .list-group-item.attached-kpi { - padding: 5px 10px; - - .attached-kpi-name { - overflow: hidden; - width: 75%; - display: inline-block; - white-space: nowrap; - text-overflow: ellipsis; - margin-top: 3px; - font-size: 13px; - font-weight: bold; - } - - .actions { - padding-top: 2px; - .alerts-config { - display: inline-block; - text-align: left; - padding-left: 4px; - height: 20px; - border-radius: 40px; - width: 21px; - color: @mblue; - background-color: white; - border: solid 1px @mblue; - - &:hover { cursor: pointer; } - } - .edit-attached-kpi { - display: inline-block; - i { - border-radius: 10px; - width: 21px; - color: @mblue; - background-color: white; - height: 20px; - padding: 3px 5px 5px 5px; - border: solid 1px @mblue; - &:hover { cursor: pointer; } - } - } - .remove-attached-kpi { - display: inline-block; - i { - border-radius: 10px; - width: 21px; - color: @brand-danger; - background-color: white; - height: 20px; - padding: 3px 5px 5px 5px; - border: solid 1px @brand-danger; - &:hover { cursor: pointer; } - } - } - } - } -} diff --git a/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html b/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html deleted file mode 100644 index ed5f2ae6..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html +++ /dev/null @@ -1,51 +0,0 @@ -
- -
-
impac.widget.settings.attach_kpis.attach_an_alert
-
- -
- - {{'impac.widget.settings.attach_kpis.keep_the' | translate}} {{elementWatched}} {{'impac.widget.settings.attach_kpis.of' | translate}} {{extraParams[selectedParam].name}}: -
-
-
- -
-
- - - -
- impac.widget.settings.attach_kpis.kpi_target_require - impac.widget.settings.attach_kpis.kpi_target_number -
-
-
- -
-
-
-
- -
-
impac.widget.settings.attach_kpis.attached_alerts
-
-
{{'impac.widget.settings.attach_kpis.keep' | translate}} {{formatAttachedKpiTitle(kpi) | titleize}}
-
- -
- -
-
-
-
- -
diff --git a/src/components/widgets/accounts-balance/accounts-balance.directive.coffee b/src/components/widgets/accounts-balance/accounts-balance.directive.coffee index a7a4b364..f3e2e71c 100644 --- a/src/components/widgets/accounts-balance/accounts-balance.directive.coffee +++ b/src/components/widgets/accounts-balance/accounts-balance.directive.coffee @@ -12,7 +12,6 @@ module.controller('WidgetAccountsBalanceCtrl', ($scope, $q, ChartFormatterSvc, $ $scope.timePeriodDeferred = $q.defer() $scope.histModeDeferred = $q.defer() $scope.chartDeferred = $q.defer() - # $scope.attachKpisDeferred = $q.defer() settingsPromises = [ $scope.orgDeferred.promise @@ -21,7 +20,6 @@ module.controller('WidgetAccountsBalanceCtrl', ($scope, $q, ChartFormatterSvc, $ $scope.timePeriodDeferred.promise $scope.histModeDeferred.promise $scope.chartDeferred.promise - # $scope.attachKpisDeferred.promise ] $scope.kpiExtraParams = {} diff --git a/src/filters/mno-currency/mno-currency.filter.coffee b/src/filters/mno-currency/mno-currency.filter.coffee index 89e0de7b..4aec0d61 100644 --- a/src/filters/mno-currency/mno-currency.filter.coffee +++ b/src/filters/mno-currency/mno-currency.filter.coffee @@ -3,30 +3,12 @@ # or in js file: # $filter('mnoCurrency')(amount,currency,[true|false]) # -angular.module('impac.filters.mno-currency', []).filter('mnoCurrency', ($filter) -> +angular.module('impac.filters.mno-currency', []).filter('mnoCurrency', ($filter, MNO_CURRENCIES) -> (amount, currency='', ISOmode=true, decimal) -> - SYMBOLS = { - USD: '$' - AUD: '$' - CAD: '$' - CNY: '¥' - EUR: '€' - GBP: '£' - HKD: '$' - INR: '' - JPY: '¥' - NZD: '$' - SGD: '$' - PHP: '₱' - AED: '' - IDR: 'Rp' - MMK: '' - } - return "" unless amount? - symbol = if !ISOmode && _.has(SYMBOLS, currency) then SYMBOLS[currency] else '' + symbol = if !ISOmode && _.has(MNO_CURRENCIES, currency) then MNO_CURRENCIES[currency] else '' s = $filter('currency')(amount, symbol, decimal) # official accounting notation: replace '(15)' by: '-15' diff --git a/src/filters/mno-currency/mno-currency.spec.js b/src/filters/mno-currency/mno-currency.spec.js index 39bb376b..3ef5ed3a 100644 --- a/src/filters/mno-currency/mno-currency.spec.js +++ b/src/filters/mno-currency/mno-currency.spec.js @@ -2,35 +2,21 @@ describe('<> mno-currency filter', function () { 'use strict'; var $filter; - - var SYMBOLS = { - USD: '$', - AUD: '$', - CAD: '$', - CNY: '¥', - EUR: '€', - GBP: '£', - HKD: '$', - INR: '', - JPY: '¥', - NZD: '$', - SGD: '$', - PHP: '₱', - AED: '', - IDR: 'Rp' - } + var MNO_CURRENCIES; beforeEach(function () { + module('maestrano.impac'); module('impac.filters.mno-currency'); - inject(function (_$filter_) { + inject(function (_$filter_, _MNO_CURRENCIES_) { $filter = _$filter_; + MNO_CURRENCIES = _MNO_CURRENCIES_; }); }); describe('when ISOmode is default or explicity true', function () { it('suffixes currencies with the correct ISO code', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { expect($filter('mnoCurrency')(1456.60, keys[i])).toContain(keys[i]); expect($filter('mnoCurrency')(1456.60, keys[i], true)).toContain(keys[i]); @@ -38,7 +24,7 @@ describe('<> mno-currency filter', function () { }); it('formats all currency types correctly', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { expect($filter('mnoCurrency')(1456.60, keys[i])).toEqual('1,456.60 ' + keys[i]); expect($filter('mnoCurrency')(1456.60, keys[i], true)).toEqual('1,456.60 ' + keys[i]); @@ -48,15 +34,15 @@ describe('<> mno-currency filter', function () { describe('when ISOmode is false', function () { it('prefixes currencies with the correct currency symbol', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { - expect($filter('mnoCurrency')(1456.60, keys[i], false)).toContain(SYMBOLS[keys[i]]); + expect($filter('mnoCurrency')(1456.60, keys[i], false)).toContain(MNO_CURRENCIES[keys[i]]); } }); it('formats all currency types correctly', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { - expect($filter('mnoCurrency')(1456.60, keys[i], false)).toEqual(SYMBOLS[keys[i]] + '1,456.60'); + expect($filter('mnoCurrency')(1456.60, keys[i], false)).toEqual(MNO_CURRENCIES[keys[i]] + '1,456.60'); } }); }); diff --git a/src/impac-angular.constant.coffee b/src/impac-angular.constant.coffee index 91b0c7e1..4079d8a0 100644 --- a/src/impac-angular.constant.coffee +++ b/src/impac-angular.constant.coffee @@ -20,3 +20,21 @@ module.constant('LOCALES', { id: 'zh-HK', name: 'Chinese (HK)' } ] ) + +module.constant('MNO_CURRENCIES', + USD: '$' + AUD: '$' + CAD: '$' + CNY: '¥' + EUR: '€' + GBP: '£' + HKD: '$' + INR: '' + JPY: '¥' + NZD: '$' + SGD: '$' + PHP: '₱' + AED: '' + IDR: 'Rp' + MMK: '' +) diff --git a/src/impac-angular.module.js b/src/impac-angular.module.js index cab00719..5a788a7e 100644 --- a/src/impac-angular.module.js +++ b/src/impac-angular.module.js @@ -113,7 +113,6 @@ angular.module('impac.components.widgets-settings', 'impac.components.widgets-settings.time-presets', 'impac.components.widgets-settings.time-slider', 'impac.components.widgets-settings.width', - 'impac.components.widgets-settings.attach-kpis', 'impac.components.widgets-settings.tag-filter', 'impac.components.widgets-settings.offsets' ] diff --git a/src/services/highcharts-factory/highcharts-factory.svc.coffee b/src/services/highcharts-factory/highcharts-factory.svc.coffee index a8d88f4b..b309fd2c 100644 --- a/src/services/highcharts-factory/highcharts-factory.svc.coffee +++ b/src/services/highcharts-factory/highcharts-factory.svc.coffee @@ -109,6 +109,10 @@ angular thresholdSerie = @findThreshold(kpiId) thresholdSerie.remove() if thresholdSerie? + updateThreshold: (threshold)-> + @removeThreshold(threshold.kpiId) + @addThreshold(threshold) + findThreshold: (kpiId)-> _.find(@hc.series, (s)-> s.options.kpiId == kpiId) diff --git a/src/services/kpis/kpis.svc.coffee b/src/services/kpis/kpis.svc.coffee index 56f6e5d5..4ef94219 100644 --- a/src/services/kpis/kpis.svc.coffee +++ b/src/services/kpis/kpis.svc.coffee @@ -108,13 +108,22 @@ angular @massAssignAll = (metadata) -> _self.load().then(-> - for k in _self.getCurrentDashboard().kpis - _self.update(k, {metadata: metadata}) - for w in _self.getCurrentDashboard().widgets - for k in w.kpis - _self.update(k, {metadata: metadata}, false) - - return + promises = [] + _.each(_self.getCurrentDashboard().kpis, (k)-> + promises.push( + _self.update(k, {metadata: metadata}).then( + (kpiData)-> + _self.applyFetchedDataToDhbKpi(k, kpiData) + ) + ) + ) + _.each(_self.getCurrentDashboard().widgets, (w)-> + w.isLoading = true + _.each(w.kpis, (k)-> + promises.push(_self.update(k, {metadata: metadata})) + ) + ) + $q.all(promises) ) @isRefreshing = false @@ -175,6 +184,12 @@ angular templ = _self.getKpiTemplate(kpiEndpoint, kpiWatchable) ((templ? && templ.target_placeholders?) && templ.target_placeholders[kpiWatchable]) || {} + @getApiV2KpiDataKey = (kpi)-> + # Formats the kpi endpoint to select the key name + # e.g response.cash_projection = { triggered: true, ... } + # TODO: maybe the 'kpi' of endpoint 'kpis/cash_projection' should be removed? + kpi.endpoint.split('kpis/').pop() + # TODO: mno & impac should be change to deal with `watchables`, instead # of element_watched, and extra_watchables. The first element of watchables should be # considered the primary watchable, a.k.a element_watched. @@ -182,6 +197,36 @@ angular return unless kpi.element_watched kpi.watchables = [kpi.element_watched].concat(kpi.extra_watchables || []) + # Logic specific to applying newly fetched data to a dhb KPI. + @applyFetchedDataToDhbKpi = (kpi, fetchedData)-> + # Calculation + kpi.data = fetchedData.kpi.calculation + + # Configuration + updatedConfig = fetchedData.kpi.configuration || {} + # When the kpi initial configuration is partial, update the extra_params with what the + # API has picked by default + kpi.extra_params = updatedConfig.extra_params if !kpi.extra_params? && updatedConfig.extra_params? + # Apply currency converted targets + kpi.targets = updatedConfig.targets + + # Layout + kpi.layout = fetchedData.kpi.layout + + # Extra Params + # Get the corresponding template of the KPI loaded + kpiTemplate = _self.getKpiTemplate(kpi.endpoint, kpi.element_watched) + # Set the kpi name from the template + kpi.name = kpiTemplate? && kpiTemplate.name + # If the template contains extra params we add it to the KPI + if kpiTemplate? && kpiTemplate.extra_params? + kpi.possibleExtraParams = kpiTemplate.extra_params + # Init the extra params select boxes with the first param + _.forIn(kpi.possibleExtraParams, (paramValues, param)-> + (kpi.extra_params ||= {})[param] = paramValues[0].id if paramValues[0] + ) + kpi + #==================================== # CRUD methods #==================================== @@ -221,8 +266,7 @@ angular kpisTemplatesPromises.push $http.get("#{bolt.path}/kpis").then( (response) -> for template in response.data.kpis - template.metadata ||= {} - template.metadata.bolt_path = bolt.path + template.source = bolt.path kpisTemplates.push(template) (error) -> $log.error("Impac! - KpisSvc: cannot retrieve kpis templates from bolt", "#{bolt.path}/kpis") @@ -252,26 +296,19 @@ angular host = ImpacRoutes.kpis.show(_self.getCurrentDashboard().id, kpi.id) when 'local' host = ImpacRoutes.kpis.local() + else + if _.isEmpty(kpi.source) + err = { message: 'Impac! - KpisSvc: cannot show a KPI without a valid source' } + $log.error(err.message) + return $q.reject(err) + # Retreive KPI from external source (bolts) + host = kpi.source url = formatShowQuery(host, kpi.endpoint, kpi.element_watched, params) return $http.get(url).then( (response) -> - kpiResp = response.data.kpi - # Calculation - kpi.data = kpiResp.calculation - - # Configuration - # When the kpi initial configuration is partial, we update it with what the API has picked by default - updatedConfig = kpiResp.configuration || {} - missingParams = _.select ['targets','extra_params'], ( (param) -> !kpi[param]? && updatedConfig[param]?) - angular.extend kpi, _.pick(updatedConfig, missingParams) - - # Layout - kpi.layout = kpiResp.layout - - return kpi - + response.data (err) -> $log.error 'Impac! - KpisSvc: Could not retrieve KPI (show) at: ' + kpi.endpoint, err $q.reject(err) @@ -281,14 +318,14 @@ angular $q.reject({error: { message: 'Impac! - KpisSvc: Service is not initialized' }}) ).finally ( -> kpi.isLoading = false ) - @create = (source, endpoint, elementWatched, opts={}) -> + @create = (kpi, opts = {}) -> deferred = $q.defer() _self.load().then( -> params = { - source: source - endpoint: endpoint - element_watched: elementWatched + source: kpi.source || 'impac' + endpoint: kpi.endpoint + element_watched: kpi.element_watched metadata: currency: _self.getCurrentDashboard().currency } @@ -311,7 +348,7 @@ angular _self.buildKpiWatchables(kpi) deferred.resolve(kpi) (err) -> - $log.error("Impac! - KpisSvc: Unable to create kpi endpoint=#{endpoint}", err) + $log.error("Impac! - KpisSvc: Unable to create kpi endpoint=#{kpi.endpoint}", err) toastr.error('Unable to create KPI', 'Error') deferred.reject(err) ) @@ -324,28 +361,22 @@ angular @update = (kpi, params = {}, showKpi = true) -> kpi.isLoading = true _self.load().then(-> - - filtered_params = {} - filtered_params.name = params.name if params.name? - filtered_params.settings = params.metadata if params.metadata? - filtered_params.targets = params.targets if params.targets? - filtered_params.extra_params = params.extra_params if params.extra_params? + filtered_params = _.pick(params, ['name', 'metadata', 'targets', 'extra_params']) + return if _.isEmpty(filtered_params) url = ImpacRoutes.kpis.update(_self.getCurrentDashboard().id, kpi.id) - - if !_.isEmpty filtered_params - $http.put(url, {kpi: params}).then( - (success) -> - # Alerts can be created by default on kpi#update (dashboard.kpis), check for - # new alerts and register them with Pusher. - ImpacEvents.notifyCallbacks(IMPAC_EVENTS.addOrRemoveAlerts) - angular.extend(kpi, success.data) - _self.buildKpiWatchables(kpi) - if showKpi then _self.show(kpi) else kpi - (err) -> - $log.error("Impac! - KpisSvc: Unable to update KPI #{kpi.id}", err) - $q.reject(err) - ) + $http.put(url, {kpi: filtered_params}).then( + (success) -> + # Alerts can be created by default on kpi#update (dashboard.kpis), check for + # new alerts and register them with Pusher. + ImpacEvents.notifyCallbacks(IMPAC_EVENTS.addOrRemoveAlerts) + angular.extend(kpi, success.data) + _self.buildKpiWatchables(kpi) + if showKpi then _self.show(kpi) else kpi + (err) -> + $log.error("Impac! - KpisSvc: Unable to update KPI #{kpi.id}", err) + $q.reject(err) + ) ).finally(-> kpi.isLoading = false ) diff --git a/src/services/widgets/widgets.svc.coffee b/src/services/widgets/widgets.svc.coffee index b3cf52d7..cbdc49d6 100644 --- a/src/services/widgets/widgets.svc.coffee +++ b/src/services/widgets/widgets.svc.coffee @@ -1,6 +1,6 @@ angular .module('impac.services.widgets', []) - .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacEvents, ImpacTheming, IMPAC_EVENTS) -> + .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacTheming, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS) -> _self = @ # ==================================== @@ -94,7 +94,7 @@ angular # @returns Promise @massAssignAll = (metadata, refreshCache=false) -> return $q.reject('undefined metadata') if _.isEmpty(metadata) - + _self.load().then( (_widget) -> currentDhb = ImpacDashboardsSvc.getCurrentDashboard() @@ -183,7 +183,7 @@ angular # By default, widget is to be fetched from legacy Impac! API (v1) dashboard = ImpacDashboardsSvc.getCurrentDashboard() ImpacRoutes.widgets.show(widget.endpoint, dashboard.id, widget.id) - + url = [route, decodeURIComponent( $.param(params) )].join('?') authHeader = 'Basic ' + btoa(_self.getSsoSessionId()) @@ -206,9 +206,22 @@ angular # Push new content to widget, and initialize it name = success.data.name angular.extend widget, { content: content, originalName: name, demoData: demoData } - initWidget(widget) - $q.resolve(widget) + # Fetches Widget KPIs calculations + kpiPromises = _.map(widget.kpis, (kpi)-> + ImpacKpisSvc.show(kpi).then( + (kpiData)-> + dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) + angular.extend(kpi, kpiData[dataKey]) + (err)-> + $log.error('Impac! - WidgetsSvc: Cannot retrieve Widget KPI: ', err) + ) + ) + $q.all(kpiPromises).then( + -> + initWidget(widget) + $q.resolve(widget) + ) (showError) -> initWidget(widget) widget.processError(showError.data.error) if angular.isDefined(widget.processError) && showError.data? && showError.data.error @@ -252,7 +265,7 @@ angular ) @update = (widget, opts, needContentReload = true) -> - widget.isLoading = needContentReload + widget.isLoading = needContentReload unless widget.isLoading _self.load().then( (_widget) -> if !isWidgetInCurrentDashboard(widget.id) @@ -273,10 +286,10 @@ angular (success) -> angular.extend widget, success.data if needContentReload - _self.show(widget) + _self.show(widget) else $q.resolve(widget) - + (updateError) -> $log.error("Impac! - WidgetsSvc: Cannot update widget: #{widget.id}") $q.reject(updateError)