(function() { 'use strict'; var directives = angular.module('redash.directives', []); directives.directive('rdTab', ['$location', function($location) { return { restrict: 'E', scope: { 'id': '@', 'name': '@' }, transclude: true, template: '
  • {{name}}
  • ', replace: true, link: function(scope) { scope.$watch(function(){return scope.$parent.selectedTab}, function(tab) { scope.selectedTab = tab; }); } } }]); directives.directive('rdTabs', ['$location', '$rootScope', function($location, $rootScope) { return { restrict: 'E', scope: { tabsCollection: '=', selectedTab: '=' }, template: '', replace: true, link: function($scope, element, attrs) { $scope.selectTab = function(tabKey) { $scope.selectedTab = _.find($scope.tabsCollection, function(tab) { return tab.key == tabKey; }); } $scope.$watch(function() { return $location.hash()}, function(hash) { if (hash) { $scope.selectTab($location.hash()); } else { $scope.selectTab($scope.tabsCollection[0].key); } }); } } }]); directives.directive('editVisulatizationForm', ['Visualization', 'growl', function(Visualization, growl) { return { restrict: 'E', templateUrl: '/views/edit_visualization.html', replace: true, scope: { query: '=', vis: '=?' }, link: function(scope, element, attrs) { scope.advancedMode = false; scope.visTypes = { 'Chart': Visualization.prototype.TYPES.CHART, 'Cohort': Visualization.prototype.TYPES.COHORT }; scope.seriesTypes = { 'Line': 'line', 'Column': 'column', 'Area': 'area', 'Scatter': 'scatter', 'Pie': 'pie' }; scope.stackingOptions = { "None": "none", "Normal": "normal", "Percent": "percent" }; scope.stacking = "none"; if (!scope.vis) { // create new visualization // wait for query to load to populate with defaults var unwatch = scope.$watch('query', function(q) { if (q && q.id) { unwatch(); if (!scope.vis) { scope.vis = { 'query_id': q.id, }; } } }, true); } function newOptions(chartType) { if (chartType === Visualization.prototype.TYPES.CHART) { return { 'series': { 'type': 'column', 'stacking': null } }; }; return {}; } var chartOptionsUnwatch = null; scope.$watch('vis.type', function(type) { if (type && type == Visualization.prototype.TYPES.CHART) { if (scope.vis.options.series.stacking === null) { scope.stacking = "none"; } else if (scope.vis.options.series.stacking === undefined) { scope.stacking = "normal"; } else { scope.stacking = scope.vis.options.series.stacking ; } chartOptionsUnwatch = scope.$watch("stacking", function(stacking) { if (stacking == "none") { scope.vis.options.series.stacking = null; } else { scope.vis.options.series.stacking = stacking; } }); } else { if (chartOptionsUnwatch) { chartOptionsUnwatch(); chartOptionsUnwatch = null; } } }); scope.toggleAdvancedMode = function() { scope.advancedMode = !scope.advancedMode; }; scope.typeChanged = function() { scope.vis.options = newOptions(scope.vis.type); }; scope.submit = function() { Visualization.save(scope.vis, function success(result) { growl.addSuccessMessage("Visualization saved"); if (updateTabs) { scope.vis = { 'query_id': scope.query.id }; var visIds = _.pluck(scope.query.visualizations, 'id'); var index = visIds.indexOf(result.id); if (index > -1) { scope.query.visualizations[index] = result; } else { scope.query.visualizations.push(result); } } }, function error() { growl.addErrorMessage("Visualization could not be saved"); }); }; } } }]); directives.directive('editDashboardForm', ['$http', '$location', '$timeout', 'Dashboard', function($http, $location, $timeout, Dashboard) { return { restrict: 'E', scope: { dashboard: '=' }, templateUrl: '/views/edit_dashboard.html', replace: true, link: function($scope, element, attrs) { var gridster = element.find(".gridster ul").gridster({ widget_margins: [5, 5], widget_base_dimensions: [260, 100], min_cols: 2, max_cols: 2, serialize_params: function($w, wgd) { return { col: wgd.col, row: wgd.row, id: $w.data('widget-id') } } }).data('gridster'); var gsItemTemplate = '
  • ' + '
    {name}' + '
  • '; $scope.$watch('dashboard.widgets', function(widgets) { $timeout(function () { gridster.remove_all_widgets(); if (widgets && widgets.length) { var layout = []; _.each(widgets, function(row, rowIndex) { _.each(row, function(widget, colIndex) { layout.push({ id: widget.id, col: colIndex+1, row: rowIndex+1, ySize: 1, xSize: widget.width, name: widget.visualization.query.name }); }); }); _.each(layout, function(item) { var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name); gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row); }); } }); }, true); $scope.saveDashboard = function() { $scope.saveInProgress = true; // TODO: we should use the dashboard service here. if ($scope.dashboard.id) { var positions = $(element).find('.gridster ul').data('gridster').serialize(); var layout = []; _.each(_.sortBy(positions, function (pos) { return pos.row * 10 + pos.col; }), function (pos) { var row = pos.row - 1; var col = pos.col - 1; layout[row] = layout[row] || []; if (col > 0 && layout[row][col - 1] == undefined) { layout[row][col - 1] = pos.id; } else { layout[row][col] = pos.id; } }); $scope.dashboard.layout = layout; layout = JSON.stringify(layout); $http.post('/api/dashboards/' + $scope.dashboard.id, {'name': $scope.dashboard.name, 'layout': layout}).success(function(response) { $scope.dashboard = new Dashboard(response); $scope.saveInProgress = false; $(element).modal('hide'); }) } else { $http.post('/api/dashboards', {'name': $scope.dashboard.name}).success(function(response) { $(element).modal('hide'); $location.path('/dashboard/' + response.slug).replace(); }) } } } } }]); directives.directive('newWidgetForm', ['$http', 'Query', function($http, Query) { return { restrict: 'E', scope: { dashboard: '=' }, templateUrl: '/views/new_widget_form.html', replace: true, link: function($scope, element, attrs) { $scope.widgetSizes = [{name: 'Regular', value: 1}, {name: 'Double', value: 2}]; var reset = function() { $scope.saveInProgress = false; $scope.widgetSize = 1; $scope.queryId = null; $scope.selectedVis = null; } reset(); $scope.loadVisualizations = function() { if (!$scope.queryId) { return; } Query.get({ id: $scope.queryId }, function(query) { if (query) { $scope.query = query; if(query.visualizations.length) { $scope.selectedVis = query.visualizations[0]; } } }); }; $scope.saveWidget = function() { $scope.saveInProgress = true; var widget = { 'visualization_id': $scope.selectedVis.id, 'dashboard_id': $scope.dashboard.id, 'options': {}, 'width': $scope.widgetSize } $http.post('/api/widgets', widget).success(function(response) { // update dashboard layout $scope.dashboard.layout = response['layout']; if (response['new_row']) { $scope.dashboard.widgets.push([response['widget']]); } else { $scope.dashboard.widgets[$scope.dashboard.widgets.length-1].push(response['widget']); } // close the dialog $('#add_query_dialog').modal('hide'); reset(); }) } } } }]) // From: http://jsfiddle.net/joshdmiller/NDFHg/ directives.directive('editInPlace', function () { return { restrict: 'E', scope: { value: '=', ignoreBlanks: '=', editable: '=' }, template: function(tElement, tAttrs) { var elType = tAttrs.editor || 'input'; var placeholder = tAttrs.placeholder || 'Click to edit'; return '' + '' + placeholder + '' + '<{elType} ng-model="value" class="form-control" rows="2">'.replace('{elType}', elType); }, link: function ($scope, element, attrs) { // Let's get a reference to the input element, as we'll want to reference it. var inputElement = angular.element(element.children()[2]); // This directive should have a set class so we can style it. element.addClass('edit-in-place'); // Initially, we're not editing. $scope.editing = false; // ng-click handler to activate edit-in-place $scope.edit = function () { if ($scope.ignoreBlanks) { $scope.oldValue = $scope.value; } $scope.editing = true; // We control display through a class on the directive itself. See the CSS. element.addClass('active'); // And we must focus the element. // `angular.element()` provides a chainable array, like jQuery so to access a native DOM function, // we have to reference the first element in the array. inputElement[0].focus(); }; $(inputElement).blur(function() { if ($scope.ignoreBlanks && _.isEmpty($scope.value)) { $scope.value = $scope.oldValue; } $scope.editing = false; element.removeClass('active'); }) } }; }); // http://stackoverflow.com/a/17904092/1559840 directives.directive('jsonText', function() { return { restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ngModel) { function into(input) { return JSON.parse(input); } function out(data) { return JSON.stringify(data, undefined, 2); } ngModel.$parsers.push(into); ngModel.$formatters.push(out); } }; }); directives.directive('rdTimer', ['$timeout', function ($timeout) { return { restrict: 'E', scope: { timestamp: '=' }, template: '{{currentTime}}', controller: ['$scope' ,function ($scope) { $scope.currentTime = "00:00:00"; var currentTimeout = null; var updateTime = function() { $scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss") currentTimeout = $timeout(updateTime, 1000); } var cancelTimer = function() { if (currentTimeout) { $timeout.cancel(currentTimeout); currentTimeout = null; } } updateTime(); $scope.$on('$destroy', function () { cancelTimer(); }); }] }; }]); })();