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 @@ -