Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPAC-639] Threshold currency #511

Open
wants to merge 5 commits into
base: 1.7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### v1.6.2 | 2017 - Week 39

#### Adds
- [IMPAC-693] Add currency conversion to KPI targets

#### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 54 additions & 71 deletions src/components/kpi/kpi.directive.coffee
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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 = ()->
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)->
Expand All @@ -155,38 +145,31 @@ angular
$scope.targets = angular.copy($scope.kpi.targets)
# smoother delete transition
$timeout ->
$scope.hideEditSettings()
hideEditSettings()
, 200

$scope.deleteKpi = ->
return if $scope.kpi.static
$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.
Expand Down
2 changes: 1 addition & 1 deletion src/components/kpis-bar/kpis-bar.directive.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Expand Down Expand Up @@ -71,44 +75,58 @@ 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()
)

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()
)
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
})
Loading