From 437b880b3e54f3f4bf6eb9b3681f5d0e5985c2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 25 Mar 2016 22:14:29 +0100 Subject: [PATCH 01/29] feat(plugins): work on setup wizard started --- public/app/core/components/grafana_app.ts | 3 + public/app/core/services/util_srv.js | 31 --------- public/app/core/services/util_srv.ts | 41 ++++++++++++ .../plugins/import_list/import_list.ts | 4 -- .../app/features/plugins/partials/wizard.html | 30 +++++++++ .../app/features/plugins/plugin_edit_ctrl.ts | 64 +++++++++++++++---- public/app/features/plugins/wizard.ts | 47 ++++++++++++++ 7 files changed, 173 insertions(+), 47 deletions(-) delete mode 100644 public/app/core/services/util_srv.js create mode 100644 public/app/core/services/util_srv.ts create mode 100644 public/app/features/plugins/partials/wizard.html create mode 100644 public/app/features/plugins/wizard.ts diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 0a2e49e5d72..5a72567c202 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -5,7 +5,9 @@ import store from 'app/core/store'; import _ from 'lodash'; import angular from 'angular'; import $ from 'jquery'; + import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; export class GrafanaCtrl { @@ -47,6 +49,7 @@ export class GrafanaCtrl { $rootScope.appEvent = function(name, payload) { $rootScope.$emit(name, payload); + appEvents.emit(name, payload); }; $rootScope.colors = [ diff --git a/public/app/core/services/util_srv.js b/public/app/core/services/util_srv.js deleted file mode 100644 index e6bf3ae08bf..00000000000 --- a/public/app/core/services/util_srv.js +++ /dev/null @@ -1,31 +0,0 @@ -define([ - 'angular', - '../core_module', -], -function (angular, coreModule) { - 'use strict'; - - coreModule.default.service('utilSrv', function($rootScope, $modal, $q) { - - this.init = function() { - $rootScope.onAppEvent('show-modal', this.showModal, $rootScope); - }; - - this.showModal = function(e, options) { - var modal = $modal({ - modalClass: options.modalClass, - template: options.src, - persist: false, - show: false, - scope: options.scope, - keyboard: false - }); - - $q.when(modal).then(function(modalEl) { - modalEl.modal('show'); - }); - }; - - }); - -}); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts new file mode 100644 index 00000000000..0f15a65a0dc --- /dev/null +++ b/public/app/core/services/util_srv.ts @@ -0,0 +1,41 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; + +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class UtilSrv { + + /** @ngInject */ + constructor(private $rootScope, private $modal) { + } + + init() { + appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope); + } + + showModal(options) { + if (options.model) { + options.scope = this.$rootScope.$new(); + options.scope.model = options.model; + } + + var modal = this.$modal({ + modalClass: options.modalClass, + template: options.src, + persist: false, + show: false, + scope: options.scope, + keyboard: false + }); + + Promise.resolve(modal).then(function(modalEl) { + modalEl.modal('show'); + }); + } +} + +coreModule.service('utilSrv', UtilSrv); diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 8eb726b81c0..607cf8cc5ec 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -88,7 +88,3 @@ export function dashboardImportList() { } coreModule.directive('dashboardImportList', dashboardImportList); - - - - diff --git a/public/app/features/plugins/partials/wizard.html b/public/app/features/plugins/partials/wizard.html new file mode 100644 index 00000000000..4a532e81a0f --- /dev/null +++ b/public/app/features/plugins/partials/wizard.html @@ -0,0 +1,30 @@ + + diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 7f3f6bff08f..59af8f78c30 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -4,6 +4,8 @@ import angular from 'angular'; import _ from 'lodash'; import appEvents from 'app/core/app_events'; +import {WizardFlow} from './wizard'; + export class PluginEditCtrl { model: any; pluginIcon: string; @@ -79,20 +81,58 @@ export class PluginEditCtrl { } update() { - this.preUpdateHook().then(() => { - var updateCmd = _.extend({ - enabled: this.model.enabled, - pinned: this.model.pinned, - jsonData: this.model.jsonData, - secureJsonData: this.model.secureJsonData, - }, {}); + var wizard = new WizardFlow("Application Setup"); - return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd); - }) - .then(this.postUpdateHook) - .then((res) => { - window.location.href = window.location.href; + wizard.addStep("Validating form", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); }); + + wizard.addStep("Saving application config", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + wizard.addStep("Validing key", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + wizard.addStep("Adding Raintank metric data source", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + wizard.addStep("Adding Raintank event data source", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + wizard.addStep("Importing worldPing dashboards", () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + wizard.start(); + // this.preUpdateHook().then(() => { + // var updateCmd = _.extend({ + // enabled: this.model.enabled, + // pinned: this.model.pinned, + // jsonData: this.model.jsonData, + // secureJsonData: this.model.secureJsonData, + // }, {}); + // return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd); + // }) + // .then(this.postUpdateHook) + // .then((res) => { + // window.location.href = window.location.href; + // }); } importDashboards() { diff --git a/public/app/features/plugins/wizard.ts b/public/app/features/plugins/wizard.ts new file mode 100644 index 00000000000..741b0f01b3e --- /dev/null +++ b/public/app/features/plugins/wizard.ts @@ -0,0 +1,47 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; + +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class WizardSrv { + + /** @ngInject */ + constructor() { + } + +} + +export class WizardStep { + name: string; + fn: any; +} + +export class WizardFlow { + name: string; + steps: WizardStep[]; + + constructor(name) { + this.name = name; + this.steps = []; + } + + addStep(name, stepFn) { + this.steps.push({ + name: name, + fn: stepFn + }); + } + + start() { + appEvents.emit('show-modal', { + src: 'public/app/features/plugins/partials/wizard.html', + model: this + }); + } +} + +coreModule.service('wizardSrv', WizardSrv); From 1f9922a5aa429a734430881bef3d3a6e69b8b3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2016 16:53:19 -0400 Subject: [PATCH 02/29] refactor(): moved dashboard_srv and DashboardCtrl to typescript --- packaging/publish/publish.sh | 16 +- public/app/features/dashboard/all.js | 3 +- .../app/features/dashboard/dashboardCtrl.js | 147 ---------- .../app/features/dashboard/dashboard_ctrl.ts | 154 ++++++++++ .../features/dashboard/dynamicDashboardSrv.js | 181 ------------ .../dashboard/dynamic_dashboard_srv.ts | 175 ++++++++++++ .../app/features/dashboard/submenu/submenu.ts | 3 +- public/test/core/utils/emitter_specs.ts | 2 +- .../test/specs/dynamicDashboardSrv-specs.js | 267 ------------------ .../test/specs/dynamic_dashboard_srv_specs.ts | 264 +++++++++++++++++ 10 files changed, 604 insertions(+), 608 deletions(-) delete mode 100644 public/app/features/dashboard/dashboardCtrl.js create mode 100644 public/app/features/dashboard/dashboard_ctrl.ts delete mode 100644 public/app/features/dashboard/dynamicDashboardSrv.js create mode 100644 public/app/features/dashboard/dynamic_dashboard_srv.ts delete mode 100644 public/test/specs/dynamicDashboardSrv-specs.js create mode 100644 public/test/specs/dynamic_dashboard_srv_specs.ts diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index dd0c935d2cd..23d20b5a613 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,22 +1,22 @@ #! /usr/bin/env bash -deb_ver=3.0.0-beta41460581169 -rpm_ver=3.0.0-beta41460581169 +deb_ver=3.0.0-beta51460658374 +rpm_ver=3.0.0-beta51460658374 #rpm_ver=3.0.0-1 -wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb +#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb #package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb #package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb -wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm +#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm -package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm -ackage_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm +#package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm +package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm # package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm # package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index d110019add6..9d370921332 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -1,5 +1,5 @@ define([ - './dashboardCtrl', + './dashboard_ctrl', './dashboardLoaderSrv', './dashnav/dashnav', './submenu/submenu', @@ -14,7 +14,6 @@ define([ './unsavedChangesSrv', './timepicker/timepicker', './graphiteImportCtrl', - './dynamicDashboardSrv', './importCtrl', './impression_store', ], function () {}); diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js deleted file mode 100644 index b6702631155..00000000000 --- a/public/app/features/dashboard/dashboardCtrl.js +++ /dev/null @@ -1,147 +0,0 @@ -define([ - 'angular', - 'jquery', - 'app/core/config', - 'moment', -], -function (angular, $, config, moment) { - "use strict"; - - var module = angular.module('grafana.controllers'); - - module.controller('DashboardCtrl', function( - $scope, - $rootScope, - dashboardKeybindings, - timeSrv, - templateValuesSrv, - dynamicDashboardSrv, - dashboardSrv, - unsavedChangesSrv, - dashboardViewStateSrv, - contextSrv, - $timeout) { - - $scope.editor = { index: 0 }; - $scope.panels = config.panels; - - var resizeEventTimeout; - - this.init = function(dashboard) { - $scope.resetRow(); - $scope.registerWindowResizeEvent(); - $scope.onAppEvent('show-json-editor', $scope.showJsonEditor); - $scope.setupDashboard(dashboard); - }; - - $scope.setupDashboard = function(data) { - $rootScope.performance.dashboardLoadStart = new Date().getTime(); - $rootScope.performance.panelsInitialized = 0; - $rootScope.performance.panelsRendered = 0; - - var dashboard = dashboardSrv.create(data.dashboard, data.meta); - dashboardSrv.setCurrent(dashboard); - - // init services - timeSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - templateValuesSrv.init(dashboard).finally(function() { - dynamicDashboardSrv.init(dashboard); - unsavedChangesSrv.init(dashboard, $scope); - - $scope.dashboard = dashboard; - $scope.dashboardMeta = dashboard.meta; - $scope.dashboardViewState = dashboardViewStateSrv.create($scope); - - dashboardKeybindings.shortcuts($scope); - - $scope.updateSubmenuVisibility(); - $scope.setWindowTitleAndTheme(); - - $scope.appEvent("dashboard-loaded", $scope.dashboard); - }).catch(function(err) { - if (err.data && err.data.message) { err.message = err.data.message; } - $scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]); - }); - }; - - $scope.updateSubmenuVisibility = function() { - $scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); - }; - - $scope.setWindowTitleAndTheme = function() { - window.document.title = config.window_title_prefix + $scope.dashboard.title; - }; - - $scope.broadcastRefresh = function() { - $rootScope.performance.panelsRendered = 0; - $rootScope.$broadcast('refresh'); - }; - - $scope.addRow = function(dash, row) { - dash.rows.push(row); - }; - - $scope.addRowDefault = function() { - $scope.resetRow(); - $scope.row.title = 'New row'; - $scope.addRow($scope.dashboard, $scope.row); - }; - - $scope.resetRow = function() { - $scope.row = { - title: '', - height: '250px', - editable: true, - }; - }; - - $scope.showJsonEditor = function(evt, options) { - var editScope = $rootScope.$new(); - editScope.object = options.object; - editScope.updateHandler = options.updateHandler; - $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); - }; - - $scope.onDrop = function(panelId, row, dropTarget) { - var info = $scope.dashboard.getPanelInfoById(panelId); - if (dropTarget) { - var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id); - dropInfo.row.panels[dropInfo.index] = info.panel; - info.row.panels[info.index] = dropTarget; - var dragSpan = info.panel.span; - info.panel.span = dropTarget.span; - dropTarget.span = dragSpan; - } - else { - info.row.panels.splice(info.index, 1); - info.panel.span = 12 - $scope.dashboard.rowSpan(row); - row.panels.push(info.panel); - } - - $rootScope.$broadcast('render'); - }; - - $scope.registerWindowResizeEvent = function() { - angular.element(window).bind('resize', function() { - $timeout.cancel(resizeEventTimeout); - resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); - }); - $scope.$on('$destroy', function() { - angular.element(window).unbind('resize'); - }); - }; - - $scope.timezoneChanged = function() { - $rootScope.$broadcast("refresh"); - }; - - $scope.formatDate = function(date) { - return moment(date).format('MMM Do YYYY, h:mm:ss a'); - }; - - }); - -}); diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts new file mode 100644 index 00000000000..f7acac4e1b7 --- /dev/null +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -0,0 +1,154 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import moment from 'moment'; +import _ from 'lodash'; + +import coreModule from 'app/core/core_module'; +import {DynamicDashboardSrv} from './dynamic_dashboard_srv'; + +export class DashboardCtrl { + + /** @ngInject */ + constructor( + private $scope, + private $rootScope, + dashboardKeybindings, + timeSrv, + templateValuesSrv, + dashboardSrv, + unsavedChangesSrv, + dashboardViewStateSrv, + contextSrv, + $timeout) { + + $scope.editor = { index: 0 }; + $scope.panels = config.panels; + $scope.dynamicDashboardSrv = new DynamicDashboardSrv(); + + var resizeEventTimeout; + + $scope.setupDashboard = function(data) { + $rootScope.performance.dashboardLoadStart = new Date().getTime(); + $rootScope.performance.panelsInitialized = 0; + $rootScope.performance.panelsRendered = 0; + + var dashboard = dashboardSrv.create(data.dashboard, data.meta); + dashboardSrv.setCurrent(dashboard); + + // init services + timeSrv.init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + templateValuesSrv.init(dashboard).finally(function() { + $scope.dynamicDashboardSrv.init(dashboard); + + unsavedChangesSrv.init(dashboard, $scope); + + $scope.dashboard = dashboard; + $scope.dashboardMeta = dashboard.meta; + $scope.dashboardViewState = dashboardViewStateSrv.create($scope); + + dashboardKeybindings.shortcuts($scope); + + $scope.updateSubmenuVisibility(); + $scope.setWindowTitleAndTheme(); + + $scope.appEvent("dashboard-loaded", $scope.dashboard); + }).catch(function(err) { + if (err.data && err.data.message) { err.message = err.data.message; } + $scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]); + }); + }; + + $scope.templateVariableUpdated = function() { + $scope.dynamicDashboardSrv.update($scope.dashboard); + }; + + $scope.updateSubmenuVisibility = function() { + $scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); + }; + + $scope.setWindowTitleAndTheme = function() { + window.document.title = config.window_title_prefix + $scope.dashboard.title; + }; + + $scope.broadcastRefresh = function() { + $rootScope.performance.panelsRendered = 0; + $rootScope.$broadcast('refresh'); + }; + + $scope.addRow = function(dash, row) { + dash.rows.push(row); + }; + + $scope.addRowDefault = function() { + $scope.resetRow(); + $scope.row.title = 'New row'; + $scope.addRow($scope.dashboard, $scope.row); + }; + + $scope.resetRow = function() { + $scope.row = { + title: '', + height: '250px', + editable: true, + }; + }; + + $scope.showJsonEditor = function(evt, options) { + var editScope = $rootScope.$new(); + editScope.object = options.object; + editScope.updateHandler = options.updateHandler; + $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); + }; + + $scope.onDrop = function(panelId, row, dropTarget) { + var info = $scope.dashboard.getPanelInfoById(panelId); + if (dropTarget) { + var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id); + dropInfo.row.panels[dropInfo.index] = info.panel; + info.row.panels[info.index] = dropTarget; + var dragSpan = info.panel.span; + info.panel.span = dropTarget.span; + dropTarget.span = dragSpan; + } else { + info.row.panels.splice(info.index, 1); + info.panel.span = 12 - $scope.dashboard.rowSpan(row); + row.panels.push(info.panel); + } + + $rootScope.$broadcast('render'); + }; + + $scope.registerWindowResizeEvent = function() { + angular.element(window).bind('resize', function() { + $timeout.cancel(resizeEventTimeout); + resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); + }); + $scope.$on('$destroy', function() { + angular.element(window).unbind('resize'); + }); + }; + + $scope.timezoneChanged = function() { + $rootScope.$broadcast("refresh"); + }; + + $scope.formatDate = function(date) { + return moment(date).format('MMM Do YYYY, h:mm:ss a'); + }; + } + + init(dashboard) { + this.$scope.resetRow(); + this.$scope.registerWindowResizeEvent(); + this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor); + this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated); + this.$scope.setupDashboard(dashboard); + } +} + +coreModule.controller('DashboardCtrl', DashboardCtrl); diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js deleted file mode 100644 index 9e369733f45..00000000000 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ /dev/null @@ -1,181 +0,0 @@ -define([ - 'angular', - 'lodash', -], -function (angular, _) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.service('dynamicDashboardSrv', function() { - var self = this; - - this.init = function(dashboard) { - if (dashboard.snapshot) { return; } - - this.iteration = new Date().getTime(); - this.process(dashboard); - }; - - this.update = function(dashboard) { - if (dashboard.snapshot) { return; } - - this.iteration = this.iteration + 1; - this.process(dashboard); - }; - - this.process = function(dashboard) { - if (dashboard.templating.list.length === 0) { return; } - this.dashboard = dashboard; - - var i, j, row, panel; - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; - // handle row repeats - if (row.repeat) { - this.repeatRow(row, i); - } - // clean up old left overs - else if (row.repeatRowId && row.repeatIteration !== this.iteration) { - this.dashboard.rows.splice(i, 1); - i = i - 1; - continue; - } - - // repeat panels - for (j = 0; j < row.panels.length; j++) { - panel = row.panels[j]; - if (panel.repeat) { - this.repeatPanel(panel, row); - } - // clean up old left overs - else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { - row.panels = _.without(row.panels, panel); - j = j - 1; - } else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) { - panel.scopedVars = {}; - } - } - } - }; - - // returns a new row clone or reuses a clone from previous iteration - this.getRowClone = function(sourceRow, repeatIndex, sourceRowIndex) { - if (repeatIndex === 0) { - return sourceRow; - } - - var i, panel, row, copy; - var sourceRowId = sourceRowIndex + 1; - - // look for row to reuse - for (i = 0; i < this.dashboard.rows.length; i++) { - row = this.dashboard.rows[i]; - if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { - copy = row; - break; - } - } - - if (!copy) { - copy = angular.copy(sourceRow); - this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy); - - // set new panel ids - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.id = this.dashboard.getNextPanelId(); - } - } - - copy.repeat = null; - copy.repeatRowId = sourceRowId; - copy.repeatIteration = this.iteration; - return copy; - }; - - // returns a new row clone or reuses a clone from previous iteration - this.repeatRow = function(row, rowIndex) { - var variables = this.dashboard.templating.list; - var variable = _.findWhere(variables, {name: row.repeat}); - if (!variable) { - return; - } - - var selected, copy, i, panel; - if (variable.current.text === 'All') { - selected = variable.options.slice(1, variable.options.length); - } else { - selected = _.filter(variable.options, {selected: true}); - } - - _.each(selected, function(option, index) { - copy = self.getRowClone(row, index, rowIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; - - for (i = 0; i < copy.panels.length; i++) { - panel = copy.panels[i]; - panel.scopedVars = {}; - panel.scopedVars[variable.name] = option; - panel.repeatIteration = this.iteration; - } - }, this); - }; - - this.getPanelClone = function(sourcePanel, row, index) { - // if first clone return source - if (index === 0) { - return sourcePanel; - } - - var i, tmpId, panel, clone; - - // first try finding an existing clone to use - for (i = 0; i < row.panels.length; i++) { - panel = row.panels[i]; - if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { - clone = panel; - break; - } - } - - if (!clone) { - clone = { id: this.dashboard.getNextPanelId() }; - row.panels.push(clone); - } - - // save id - tmpId = clone.id; - // copy properties from source - angular.copy(sourcePanel, clone); - // restore id - clone.id = tmpId; - clone.repeatIteration = this.iteration; - clone.repeatPanelId = sourcePanel.id; - clone.repeat = null; - return clone; - }; - - this.repeatPanel = function(panel, row) { - var variables = this.dashboard.templating.list; - var variable = _.findWhere(variables, {name: panel.repeat}); - if (!variable) { return; } - - var selected; - if (variable.current.text === 'All') { - selected = variable.options.slice(1, variable.options.length); - } else { - selected = _.filter(variable.options, {selected: true}); - } - - _.each(selected, function(option, index) { - var copy = self.getPanelClone(panel, row, index); - copy.span = Math.max(12 / selected.length, panel.minSpan); - copy.scopedVars = copy.scopedVars || {}; - copy.scopedVars[variable.name] = option; - }); - }; - - }); -}); diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts new file mode 100644 index 00000000000..340bca69b40 --- /dev/null +++ b/public/app/features/dashboard/dynamic_dashboard_srv.ts @@ -0,0 +1,175 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import _ from 'lodash'; + +export class DynamicDashboardSrv { + iteration: number; + dashboard: any; + + init(dashboard) { + if (dashboard.snapshot) { return; } + + this.iteration = new Date().getTime(); + this.process(dashboard); + } + + update(dashboard) { + if (dashboard.snapshot) { return; } + + this.iteration = this.iteration + 1; + this.process(dashboard); + } + + process(dashboard) { + if (dashboard.templating.list.length === 0) { return; } + this.dashboard = dashboard; + + var i, j, row, panel; + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; + // handle row repeats + if (row.repeat) { + this.repeatRow(row, i); + } else if (row.repeatRowId && row.repeatIteration !== this.iteration) { + // clean up old left overs + this.dashboard.rows.splice(i, 1); + i = i - 1; + continue; + } + + // repeat panels + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.repeat) { + this.repeatPanel(panel, row); + } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + // clean up old left overs + row.panels = _.without(row.panels, panel); + j = j - 1; + } else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) { + panel.scopedVars = {}; + } + } + } + } + + // returns a new row clone or reuses a clone from previous iteration + getRowClone(sourceRow, repeatIndex, sourceRowIndex) { + if (repeatIndex === 0) { + return sourceRow; + } + + var i, panel, row, copy; + var sourceRowId = sourceRowIndex + 1; + + // look for row to reuse + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; + if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { + copy = row; + break; + } + } + + if (!copy) { + copy = angular.copy(sourceRow); + this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy); + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.id = this.dashboard.getNextPanelId(); + } + } + + copy.repeat = null; + copy.repeatRowId = sourceRowId; + copy.repeatIteration = this.iteration; + return copy; + } + + // returns a new row clone or reuses a clone from previous iteration + repeatRow(row, rowIndex) { + var variables = this.dashboard.templating.list; + var variable = _.findWhere(variables, {name: row.repeat}); + if (!variable) { + return; + } + + var selected, copy, i, panel; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, (option, index) => { + copy = this.getRowClone(row, index, rowIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + panel.repeatIteration = this.iteration; + } + }); + } + + getPanelClone(sourcePanel, row, index) { + // if first clone return source + if (index === 0) { + return sourcePanel; + } + + var i, tmpId, panel, clone; + + // first try finding an existing clone to use + for (i = 0; i < row.panels.length; i++) { + panel = row.panels[i]; + if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { + clone = panel; + break; + } + } + + if (!clone) { + clone = { id: this.dashboard.getNextPanelId() }; + row.panels.push(clone); + } + + // save id + tmpId = clone.id; + // copy properties from source + angular.copy(sourcePanel, clone); + // restore id + clone.id = tmpId; + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + return clone; + } + + repeatPanel(panel, row) { + var variables = this.dashboard.templating.list; + var variable = _.findWhere(variables, {name: panel.repeat}); + if (!variable) { return; } + + var selected; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, (option, index) => { + var copy = this.getPanelClone(panel, row, index); + copy.span = Math.max(12 / selected.length, panel.minSpan); + copy.scopedVars = copy.scopedVars || {}; + copy.scopedVars[variable.name] = option; + }); + } +} diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index a9899c3a4b8..8e7984a4085 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -8,7 +8,7 @@ export class SubmenuCtrl { dashboard: any; /** @ngInject */ - constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) { + constructor(private $rootScope, private templateValuesSrv) { this.annotations = this.dashboard.templating.list; this.variables = this.dashboard.templating.list; } @@ -24,7 +24,6 @@ export class SubmenuCtrl { variableUpdated(variable) { this.templateValuesSrv.variableUpdated(variable).then(() => { - this.dynamicDashboardSrv.update(this.dashboard); this.$rootScope.$emit('template-variable-value-updated'); this.$rootScope.$broadcast('refresh'); }); diff --git a/public/test/core/utils/emitter_specs.ts b/public/test/core/utils/emitter_specs.ts index f7076c46719..fec4d02a649 100644 --- a/public/test/core/utils/emitter_specs.ts +++ b/public/test/core/utils/emitter_specs.ts @@ -24,7 +24,7 @@ describe("Emitter", () => { expect(sub2Called).to.be(true); }); - it.only('should handle errors', () => { + it('should handle errors', () => { var events = new Emitter(); var sub1Called = 0; var sub2Called = 0; diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js deleted file mode 100644 index b988203009a..00000000000 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ /dev/null @@ -1,267 +0,0 @@ -define([ - 'app/features/dashboard/dynamicDashboardSrv', - 'app/features/dashboard/dashboardSrv' -], function() { - 'use strict'; - - function dynamicDashScenario(desc, func) { - - describe(desc, function() { - var ctx = {}; - - ctx.setup = function (setupFunc) { - - beforeEach(module('grafana.services')); - beforeEach(module(function($provide) { - $provide.value('contextSrv', { - user: { timezone: 'utc'} - }); - })); - - beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { - ctx.dynamicDashboardSrv = dynamicDashboardSrv; - ctx.dashboardSrv = dashboardSrv; - - var model = { - rows: [], - templating: { list: [] } - }; - - setupFunc(model); - ctx.dash = ctx.dashboardSrv.create(model); - ctx.dynamicDashboardSrv.init(ctx.dash); - ctx.rows = ctx.dash.rows; - })); - }; - - func(ctx); - }); - } - - dynamicDashScenario('given dashboard with panel repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - panels: [{id: 2, repeat: 'apps'}] - }); - dash.templating.list.push({ - name: 'apps', - current: { - text: 'se1, se2, se3', - value: ['se1', 'se2', 'se3'] - }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - {text: 'se3', value: 'se3', selected: true}, - {text: 'se4', value: 'se4', selected: false} - ] - }); - }); - - it('should repeat panel one time', function() { - expect(ctx.rows[0].panels.length).to.be(3); - }); - - it('should mark panel repeated', function() { - expect(ctx.rows[0].panels[0].repeat).to.be('apps'); - expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); - }); - - it('should set scopedVars on panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); - expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); - expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3'); - }); - - describe('After a second iteration', function() { - var repeatedPanelAfterIteration1; - - beforeEach(function() { - repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; - ctx.rows[0].panels[0].fill = 10; - ctx.dynamicDashboardSrv.update(ctx.dash); - }); - - it('should have reused same panel instances', function() { - expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); - }); - - it('reused panel should copy properties from source', function() { - expect(ctx.rows[0].panels[1].fill).to.be(10); - }); - - it('should have same panel count', function() { - expect(ctx.rows[0].panels.length).to.be(3); - }); - }); - - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { - ctx.dash.templating.list[0].options[1].selected = false; - - ctx.dynamicDashboardSrv.update(ctx.dash); - }); - - it('should clean up repeated panel', function() { - expect(ctx.rows[0].panels.length).to.be(2); - }); - }); - - describe('After a second iteration and panel repeat is turned off', function() { - beforeEach(function() { - ctx.rows[0].panels[0].repeat = null; - ctx.dynamicDashboardSrv.update(ctx.dash); - }); - - it('should clean up repeated panel', function() { - expect(ctx.rows[0].panels.length).to.be(1); - }); - - it('should remove scoped vars from reused panel', function() { - expect(ctx.rows[0].panels[0].scopedVars).to.be.empty(); - }); - }); - - }); - - dynamicDashScenario('given dashboard with row repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - repeat: 'servers', - panels: [{id: 2}] - }); - dash.rows.push({panels: []}); - dash.templating.list.push({ - name: 'servers', - current: { - text: 'se1, se2', - value: ['se1', 'se2'] - }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - ] - }); - }); - - it('should repeat row one time', function() { - expect(ctx.rows.length).to.be(3); - }); - - it('should keep panel ids on first row', function() { - expect(ctx.rows[0].panels[0].id).to.be(2); - }); - - it('should keep first row as repeat', function() { - expect(ctx.rows[0].repeat).to.be('servers'); - }); - - it('should clear repeat field on repeated row', function() { - expect(ctx.rows[1].repeat).to.be(null); - }); - - it('should add scopedVars to rows', function() { - expect(ctx.rows[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].scopedVars.servers.value).to.be('se2'); - }); - - it('should generate a repeartRowId based on repeat row index', function() { - expect(ctx.rows[1].repeatRowId).to.be(1); - }); - - it('should set scopedVars on row panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); - }); - - describe('After a second iteration', function() { - var repeatedRowAfterFirstIteration; - - beforeEach(function() { - repeatedRowAfterFirstIteration = ctx.rows[1]; - ctx.rows[0].height = 500; - ctx.dynamicDashboardSrv.update(ctx.dash); - }); - - it('should still only have 2 rows', function() { - expect(ctx.rows.length).to.be(3); - }); - - it.skip('should have updated props from source', function() { - expect(ctx.rows[1].height).to.be(500); - }); - - it('should reuse row instance', function() { - expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); - }); - }); - - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { - ctx.dash.templating.list[0].options[1].selected = false; - ctx.dynamicDashboardSrv.update(ctx.dash); - }); - - it('should remove repeated second row', function() { - expect(ctx.rows.length).to.be(2); - }); - }); - }); - - dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) { - ctx.setup(function(dash) { - dash.rows.push({ - repeat: 'servers', - panels: [{id: 2, repeat: 'metric'}] - }); - dash.templating.list.push({ - name: 'servers', - current: { text: 'se1, se2', value: ['se1', 'se2'] }, - options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - ] - }); - dash.templating.list.push({ - name: 'metric', - current: { text: 'm1, m2', value: ['m1', 'm2'] }, - options: [ - {text: 'm1', value: 'm1', selected: true}, - {text: 'm2', value: 'm2', selected: true}, - ] - }); - }); - - it('should repeat row one time', function() { - expect(ctx.rows.length).to.be(2); - }); - - it('should repeat panel on both rows', function() { - expect(ctx.rows[0].panels.length).to.be(2); - expect(ctx.rows[1].panels.length).to.be(2); - }); - - it('should keep panel ids on first row', function() { - expect(ctx.rows[0].panels[0].id).to.be(2); - }); - - it('should mark second row as repeated', function() { - expect(ctx.rows[0].repeat).to.be('servers'); - }); - - it('should clear repeat field on repeated row', function() { - expect(ctx.rows[1].repeat).to.be(null); - }); - - it('should generate a repeartRowId based on repeat row index', function() { - expect(ctx.rows[1].repeatRowId).to.be(1); - }); - - it('should set scopedVars on row panels', function() { - expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); - expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); - }); - - }); - -}); diff --git a/public/test/specs/dynamic_dashboard_srv_specs.ts b/public/test/specs/dynamic_dashboard_srv_specs.ts new file mode 100644 index 00000000000..6f233fcb9ff --- /dev/null +++ b/public/test/specs/dynamic_dashboard_srv_specs.ts @@ -0,0 +1,264 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +import 'app/features/dashboard/dashboardSrv'; +import {DynamicDashboardSrv} from '../../app/features/dashboard/dynamic_dashboard_srv'; + +function dynamicDashScenario(desc, func) { + + describe(desc, function() { + var ctx: any = {}; + + ctx.setup = function (setupFunc) { + + beforeEach(angularMocks.module('grafana.services')); + beforeEach(angularMocks.module(function($provide) { + $provide.value('contextSrv', { + user: { timezone: 'utc'} + }); + })); + + beforeEach(angularMocks.inject(function(dashboardSrv) { + ctx.dashboardSrv = dashboardSrv; + var model = { + rows: [], + templating: { list: [] } + }; + + setupFunc(model); + ctx.dash = ctx.dashboardSrv.create(model); + ctx.dynamicDashboardSrv = new DynamicDashboardSrv(); + ctx.dynamicDashboardSrv.init(ctx.dash); + ctx.rows = ctx.dash.rows; + })); + }; + + func(ctx); + }); +} + +dynamicDashScenario('given dashboard with panel repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + panels: [{id: 2, repeat: 'apps'}] + }); + dash.templating.list.push({ + name: 'apps', + current: { + text: 'se1, se2, se3', + value: ['se1', 'se2', 'se3'] + }, + options: [ + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + {text: 'se3', value: 'se3', selected: true}, + {text: 'se4', value: 'se4', selected: false} + ] + }); + }); + + it('should repeat panel one time', function() { + expect(ctx.rows[0].panels.length).to.be(3); + }); + + it('should mark panel repeated', function() { + expect(ctx.rows[0].panels[0].repeat).to.be('apps'); + expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2); + }); + + it('should set scopedVars on panels', function() { + expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1'); + expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2'); + expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3'); + }); + + describe('After a second iteration', function() { + var repeatedPanelAfterIteration1; + + beforeEach(function() { + repeatedPanelAfterIteration1 = ctx.rows[0].panels[1]; + ctx.rows[0].panels[0].fill = 10; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should have reused same panel instances', function() { + expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1); + }); + + it('reused panel should copy properties from source', function() { + expect(ctx.rows[0].panels[1].fill).to.be(10); + }); + + it('should have same panel count', function() { + expect(ctx.rows[0].panels.length).to.be(3); + }); + }); + + describe('After a second iteration and selected values reduced', function() { + beforeEach(function() { + ctx.dash.templating.list[0].options[1].selected = false; + + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should clean up repeated panel', function() { + expect(ctx.rows[0].panels.length).to.be(2); + }); + }); + + describe('After a second iteration and panel repeat is turned off', function() { + beforeEach(function() { + ctx.rows[0].panels[0].repeat = null; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should clean up repeated panel', function() { + expect(ctx.rows[0].panels.length).to.be(1); + }); + + it('should remove scoped vars from reused panel', function() { + expect(ctx.rows[0].panels[0].scopedVars).to.be.empty(); + }); + }); + +}); + +dynamicDashScenario('given dashboard with row repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + repeat: 'servers', + panels: [{id: 2}] + }); + dash.rows.push({panels: []}); + dash.templating.list.push({ + name: 'servers', + current: { + text: 'se1, se2', + value: ['se1', 'se2'] + }, + options: [ + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + ] + }); + }); + + it('should repeat row one time', function() { + expect(ctx.rows.length).to.be(3); + }); + + it('should keep panel ids on first row', function() { + expect(ctx.rows[0].panels[0].id).to.be(2); + }); + + it('should keep first row as repeat', function() { + expect(ctx.rows[0].repeat).to.be('servers'); + }); + + it('should clear repeat field on repeated row', function() { + expect(ctx.rows[1].repeat).to.be(null); + }); + + it('should add scopedVars to rows', function() { + expect(ctx.rows[0].scopedVars.servers.value).to.be('se1'); + expect(ctx.rows[1].scopedVars.servers.value).to.be('se2'); + }); + + it('should generate a repeartRowId based on repeat row index', function() { + expect(ctx.rows[1].repeatRowId).to.be(1); + }); + + it('should set scopedVars on row panels', function() { + expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); + expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); + }); + + describe('After a second iteration', function() { + var repeatedRowAfterFirstIteration; + + beforeEach(function() { + repeatedRowAfterFirstIteration = ctx.rows[1]; + ctx.rows[0].height = 500; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should still only have 2 rows', function() { + expect(ctx.rows.length).to.be(3); + }); + + it.skip('should have updated props from source', function() { + expect(ctx.rows[1].height).to.be(500); + }); + + it('should reuse row instance', function() { + expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration); + }); + }); + + describe('After a second iteration and selected values reduced', function() { + beforeEach(function() { + ctx.dash.templating.list[0].options[1].selected = false; + ctx.dynamicDashboardSrv.update(ctx.dash); + }); + + it('should remove repeated second row', function() { + expect(ctx.rows.length).to.be(2); + }); + }); +}); + +dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) { + ctx.setup(function(dash) { + dash.rows.push({ + repeat: 'servers', + panels: [{id: 2, repeat: 'metric'}] + }); + dash.templating.list.push({ + name: 'servers', + current: { text: 'se1, se2', value: ['se1', 'se2'] }, + options: [ + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + ] + }); + dash.templating.list.push({ + name: 'metric', + current: { text: 'm1, m2', value: ['m1', 'm2'] }, + options: [ + {text: 'm1', value: 'm1', selected: true}, + {text: 'm2', value: 'm2', selected: true}, + ] + }); + }); + + it('should repeat row one time', function() { + expect(ctx.rows.length).to.be(2); + }); + + it('should repeat panel on both rows', function() { + expect(ctx.rows[0].panels.length).to.be(2); + expect(ctx.rows[1].panels.length).to.be(2); + }); + + it('should keep panel ids on first row', function() { + expect(ctx.rows[0].panels[0].id).to.be(2); + }); + + it('should mark second row as repeated', function() { + expect(ctx.rows[0].repeat).to.be('servers'); + }); + + it('should clear repeat field on repeated row', function() { + expect(ctx.rows[1].repeat).to.be(null); + }); + + it('should generate a repeartRowId based on repeat row index', function() { + expect(ctx.rows[1].repeatRowId).to.be(1); + }); + + it('should set scopedVars on row panels', function() { + expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1'); + expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2'); + }); + +}); + From 4d0b14fbb45407caf2fa684215ca1d7d92b9ab9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2016 17:31:34 -0400 Subject: [PATCH 03/29] feat(exporter): stared work on dashboard exporter that cleans up repeated panels etc --- .../app/features/dashboard/dashboard_ctrl.ts | 4 -- .../app/features/dashboard/dashnav/dashnav.ts | 7 +-- .../dashboard/dynamic_dashboard_srv.ts | 22 ++++++--- public/app/features/dashboard/exporter.ts | 28 ++++++++++++ .../features/dashboard/partials/settings.html | 4 +- .../specs/dynamic_dashboard_srv_specs.ts | 2 +- .../dashboard/specs/exporter_specs.ts | 45 +++++++++++++++++++ 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 public/app/features/dashboard/exporter.ts rename public/{test => app/features/dashboard}/specs/dynamic_dashboard_srv_specs.ts (98%) create mode 100644 public/app/features/dashboard/specs/exporter_specs.ts diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index f7acac4e1b7..7bb50f58bf1 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -136,10 +136,6 @@ export class DashboardCtrl { $scope.timezoneChanged = function() { $rootScope.$broadcast("refresh"); }; - - $scope.formatDate = function(date) { - return moment(date).format('MMM Do YYYY, h:mm:ss a'); - }; } init(dashboard) { diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 2d5b66cb13c..afad15afe87 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -4,6 +4,8 @@ import _ from 'lodash'; import moment from 'moment'; import angular from 'angular'; +import {DashboardExporter} from '../exporter'; + export class DashNavCtrl { /** @ngInject */ @@ -170,9 +172,8 @@ export class DashNavCtrl { $scope.exportDashboard = function() { var clone = $scope.dashboard.getSaveModelClone(); - var blob = new Blob([angular.toJson(clone, true)], { type: "application/json;charset=utf-8" }); - var wnd: any = window; - wnd.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime()); + var exporter = new DashboardExporter(); + exporter.export(clone); }; $scope.snapshot = function() { diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts index 340bca69b40..a5fb73d099a 100644 --- a/public/app/features/dashboard/dynamic_dashboard_srv.ts +++ b/public/app/features/dashboard/dynamic_dashboard_srv.ts @@ -8,30 +8,36 @@ export class DynamicDashboardSrv { iteration: number; dashboard: any; + constructor() { + this.iteration = new Date().getTime(); + } + init(dashboard) { if (dashboard.snapshot) { return; } - - this.iteration = new Date().getTime(); - this.process(dashboard); + this.process(dashboard, {}); } update(dashboard) { if (dashboard.snapshot) { return; } this.iteration = this.iteration + 1; - this.process(dashboard); + this.process(dashboard, {}); } - process(dashboard) { + process(dashboard, options) { if (dashboard.templating.list.length === 0) { return; } this.dashboard = dashboard; + var cleanUpOnly = options.cleanUpOnly; + var i, j, row, panel; for (i = 0; i < this.dashboard.rows.length; i++) { row = this.dashboard.rows[i]; // handle row repeats if (row.repeat) { - this.repeatRow(row, i); + if (!cleanUpOnly) { + this.repeatRow(row, i); + } } else if (row.repeatRowId && row.repeatIteration !== this.iteration) { // clean up old left overs this.dashboard.rows.splice(i, 1); @@ -43,7 +49,9 @@ export class DynamicDashboardSrv { for (j = 0; j < row.panels.length; j++) { panel = row.panels[j]; if (panel.repeat) { - this.repeatPanel(panel, row); + if (!cleanUpOnly) { + this.repeatPanel(panel, row); + } } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { // clean up old left overs row.panels = _.without(row.panels, panel); diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/exporter.ts new file mode 100644 index 00000000000..0a9ac2ff2ca --- /dev/null +++ b/public/app/features/dashboard/exporter.ts @@ -0,0 +1,28 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import _ from 'lodash'; + +import {DynamicDashboardSrv} from './dynamic_dashboard_srv'; + +export class DashboardExporter { + + makeExportable(dashboard) { + var dynSrv = new DynamicDashboardSrv(); + dynSrv.process(dashboard, {cleanUpOnly: true}); + + return dashboard; + } + + export(dashboard) { + var clean = this.makeExportable(dashboard); + var blob = new Blob([angular.toJson(clean, true)], { type: "application/json;charset=utf-8" }); + var wnd: any = window; + wnd.saveAs(blob, clean.title + '-' + new Date().getTime()); + } + +} + + + diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 2a2287613ae..d641e21cfdb 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -104,7 +104,7 @@
Last updated at: - {{formatDate(dashboardMeta.updated)}} + {{dashboard.formatDate(dashboardMeta.updated)}}
Last updated by: @@ -112,7 +112,7 @@
Created at: - {{formatDate(dashboardMeta.created)}}  + {{dashboard.formatDate(dashboardMeta.created)}} 
Created by: diff --git a/public/test/specs/dynamic_dashboard_srv_specs.ts b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts similarity index 98% rename from public/test/specs/dynamic_dashboard_srv_specs.ts rename to public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts index 6f233fcb9ff..ee2fac16ba1 100644 --- a/public/test/specs/dynamic_dashboard_srv_specs.ts +++ b/public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts @@ -1,7 +1,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; import 'app/features/dashboard/dashboardSrv'; -import {DynamicDashboardSrv} from '../../app/features/dashboard/dynamic_dashboard_srv'; +import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; function dynamicDashScenario(desc, func) { diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts new file mode 100644 index 00000000000..7831905fda8 --- /dev/null +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -0,0 +1,45 @@ +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +import {DashboardExporter} from '../exporter'; + +describe('given dashboard with repeated panels', function() { + var dash, exported; + + beforeEach(() => { + dash = { + rows: [], + templating: { list: [] } + }; + dash.templating.list.push({ + name: 'apps', + current: {}, + options: [] + }); + + dash.rows.push({ + repeat: 'test', + panels: [ + {id: 2, repeat: 'apps'}, + {id: 2, repeat: null, repeatPanelId: 2}, + ] + }); + dash.rows.push({ + repeat: null, + repeatRowId: 1 + }); + + var exporter = new DashboardExporter(); + exported = exporter.makeExportable(dash); + }); + + + it('exported dashboard should not contain repeated panels', function() { + expect(exported.rows[0].panels.length).to.be(1); + }); + + it('exported dashboard should not contain repeated rows', function() { + expect(exported.rows.length).to.be(1); + }); + +}); + From 0f71838fdf7bfad029f16e371e98717f70ef759e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Apr 2016 21:13:01 -0400 Subject: [PATCH 04/29] feat(dash export): dashboard export can now replace datasource names with variable and add inputs section --- .../app/features/dashboard/dashnav/dashnav.ts | 4 +- .../dashboard/dynamic_dashboard_srv.ts | 3 ++ public/app/features/dashboard/exporter.ts | 49 +++++++++++++++---- .../dashboard/specs/exporter_specs.ts | 23 +++++++-- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index afad15afe87..e9ef96408ad 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -9,7 +9,7 @@ import {DashboardExporter} from '../exporter'; export class DashNavCtrl { /** @ngInject */ - constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) { + constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); @@ -172,7 +172,7 @@ export class DashNavCtrl { $scope.exportDashboard = function() { var clone = $scope.dashboard.getSaveModelClone(); - var exporter = new DashboardExporter(); + var exporter = new DashboardExporter(datasourceSrv); exporter.export(clone); }; diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts index a5fb73d099a..747c15479b2 100644 --- a/public/app/features/dashboard/dynamic_dashboard_srv.ts +++ b/public/app/features/dashboard/dynamic_dashboard_srv.ts @@ -4,6 +4,8 @@ import config from 'app/core/config'; import angular from 'angular'; import _ from 'lodash'; +import coreModule from 'app/core/core_module'; + export class DynamicDashboardSrv { iteration: number; dashboard: any; @@ -181,3 +183,4 @@ export class DynamicDashboardSrv { }); } } + diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/exporter.ts index 0a9ac2ff2ca..04f4e0a203d 100644 --- a/public/app/features/dashboard/exporter.ts +++ b/public/app/features/dashboard/exporter.ts @@ -8,21 +8,50 @@ import {DynamicDashboardSrv} from './dynamic_dashboard_srv'; export class DashboardExporter { - makeExportable(dashboard) { - var dynSrv = new DynamicDashboardSrv(); - dynSrv.process(dashboard, {cleanUpOnly: true}); + constructor(private datasourceSrv) { + } - return dashboard; + makeExportable(dash) { + var dynSrv = new DynamicDashboardSrv(); + dynSrv.process(dash, {cleanUpOnly: true}); + + var inputs = []; + var datasources = {}; + var promises = []; + + for (let row of dash.rows) { + _.each(row.panels, (panel) => { + if (panel.datasource !== undefined) { + promises.push(this.datasourceSrv.get(panel.datasource).then(ds => { + var refName = 'DS_' + ds.name.toUpperCase(); + datasources[panel.datasource] = { + name: refName, + type: 'datasource', + pluginId: ds.meta.id, + }; + panel.datasource = '${' + refName +'}'; + })); + } + }); + } + + return Promise.all(promises).then(() => { + _.each(datasources, (value, key) => { + inputs.push(value); + }); + + dash["__inputs"] = inputs; + return dash; + }); } export(dashboard) { - var clean = this.makeExportable(dashboard); - var blob = new Blob([angular.toJson(clean, true)], { type: "application/json;charset=utf-8" }); - var wnd: any = window; - wnd.saveAs(blob, clean.title + '-' + new Date().getTime()); + return this.makeExportable(dashboard).then(clean => { + var blob = new Blob([angular.toJson(clean, true)], { type: "application/json;charset=utf-8" }); + var wnd: any = window; + wnd.saveAs(blob, clean.title + '-' + new Date().getTime()); + }); } } - - diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 7831905fda8..190ad85743a 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -5,7 +5,7 @@ import {DashboardExporter} from '../exporter'; describe('given dashboard with repeated panels', function() { var dash, exported; - beforeEach(() => { + beforeEach((done) => { dash = { rows: [], templating: { list: [] } @@ -19,7 +19,7 @@ describe('given dashboard with repeated panels', function() { dash.rows.push({ repeat: 'test', panels: [ - {id: 2, repeat: 'apps'}, + {id: 2, repeat: 'apps', datasource: 'gfdb'}, {id: 2, repeat: null, repeatPanelId: 2}, ] }); @@ -28,8 +28,18 @@ describe('given dashboard with repeated panels', function() { repeatRowId: 1 }); - var exporter = new DashboardExporter(); - exported = exporter.makeExportable(dash); + var datasourceSrvStub = { + get: sinon.stub().returns(Promise.resolve({ + name: 'gfdb', + meta: {id: "testdb"} + })) + }; + + var exporter = new DashboardExporter(datasourceSrvStub); + exporter.makeExportable(dash).then(clean => { + exported = clean; + done(); + }); }); @@ -41,5 +51,10 @@ describe('given dashboard with repeated panels', function() { expect(exported.rows.length).to.be(1); }); + it('should replace datasource refs', function() { + var panel = exported.rows[0].panels[0]; + expect(panel.datasource).to.be("${DS_GFDB}"); + }); + }); From bb6f4fff87a588c4231ce9dcfd134fae498d1da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 May 2016 16:40:21 +0200 Subject: [PATCH 05/29] feat(export/import): minor progress --- pkg/api/api.go | 1 + public/app/features/dashboard/partials/import.html | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 684633e0bcd..2b1eaa714e3 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -55,6 +55,7 @@ func Register(r *macaron.Macaron) { r.Get("/dashboard/*", reqSignedIn, Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) + r.Get("/import/dashboard", reqSignedIn, Index) r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/*", reqSignedIn, Index) diff --git a/public/app/features/dashboard/partials/import.html b/public/app/features/dashboard/partials/import.html index 3f23e4cb682..ecf015b18cc 100644 --- a/public/app/features/dashboard/partials/import.html +++ b/public/app/features/dashboard/partials/import.html @@ -4,15 +4,19 @@
+
+ Upload .json file +
+
-
-
-
+
+
+ +
From 79a8017fe916698f1979e68d591ef2ef96bf1417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 20:31:47 +0200 Subject: [PATCH 06/29] feat(export): more progress on dashboard export --- public/app/features/dashboard/exporter.ts | 14 ++++++++++++++ .../app/features/dashboard/specs/exporter_specs.ts | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/exporter.ts index c1f5a13fd9b..8ed3f950c7f 100644 --- a/public/app/features/dashboard/exporter.ts +++ b/public/app/features/dashboard/exporter.ts @@ -16,6 +16,7 @@ export class DashboardExporter { dynSrv.process(dash, {cleanUpOnly: true}); var inputs = []; + var requires = {}; var datasources = {}; var promises = []; @@ -30,6 +31,13 @@ export class DashboardExporter { pluginId: ds.meta.id, }; panel.datasource = '${' + refName +'}'; + + requires['datasource' + ds.meta.id] = { + type: 'datasource', + id: ds.meta.id, + name: ds.meta.name, + version: ds.meta.info.version + }; })); } }); @@ -40,7 +48,13 @@ export class DashboardExporter { inputs.push(value); }); + requires = _.map(requires, req => { + return req; + }); + dash["__inputs"] = inputs; + dash["__requires"] = requires; + return dash; }); } diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 190ad85743a..5b0576b729c 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co import {DashboardExporter} from '../exporter'; -describe('given dashboard with repeated panels', function() { +describe.only('given dashboard with repeated panels', function() { var dash, exported; beforeEach((done) => { @@ -39,6 +39,7 @@ describe('given dashboard with repeated panels', function() { exporter.makeExportable(dash).then(clean => { exported = clean; done(); + console.log('done'); }); }); @@ -56,5 +57,11 @@ describe('given dashboard with repeated panels', function() { expect(panel.datasource).to.be("${DS_GFDB}"); }); + it('should add datasource as input', function() { + expect(exported.__inputs[0].name).to.be("DS_GFDB"); + expect(exported.__inputs[0].pluginId).to.be("testdb"); + expect(exported.__inputs[0].type).to.be("datasource"); + }); + }); From 5b42753b8b135bcdc987ba6417b990c348106871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 21:09:15 +0200 Subject: [PATCH 07/29] feat(export): progress on dashboard export --- karma.conf.js | 2 +- public/app/features/dashboard/exporter.ts | 25 ++++++++++++--- .../dashboard/specs/exporter_specs.ts | 31 ++++++++++++++++--- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index c803dda5eae..cdcea23a90b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -26,7 +26,7 @@ module.exports = function(config) { browsers: ['PhantomJS'], captureTimeout: 20000, singleRun: true, - autoWatchBatchDelay: 10000, + autoWatchBatchDelay: 1000, browserNoActivityTimeout: 60000, }); diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/exporter.ts index 8ed3f950c7f..9a8d5bae5b1 100644 --- a/public/app/features/dashboard/exporter.ts +++ b/public/app/features/dashboard/exporter.ts @@ -36,10 +36,20 @@ export class DashboardExporter { type: 'datasource', id: ds.meta.id, name: ds.meta.name, - version: ds.meta.info.version + version: ds.meta.info.version || "1.0.0", }; })); } + + var panelDef = config.panels[panel.type]; + if (panelDef) { + requires['panel' + panelDef.id] = { + type: 'panel', + id: panelDef.id, + name: panelDef.name, + version: panelDef.info.version, + }; + } }); } @@ -56,14 +66,21 @@ export class DashboardExporter { dash["__requires"] = requires; return dash; + }).catch(err => { + console.log('Export failed:', err); + return {}; }); } export(dashboard) { return this.makeExportable(dashboard).then(clean => { - var blob = new Blob([angular.toJson(clean, true)], { type: "application/json;charset=utf-8" }); - var wnd: any = window; - wnd.saveAs(blob, clean.title + '-' + new Date().getTime() + '.json'); + var html = angular.toJson(clean, true); + var uri = "data:application/json," + encodeURIComponent(html); + var newWindow = window.open(uri); + + // var blob = new Blob([angular.toJson(clean, true)], { type: "application/json;charset=utf-8" }); + // var wnd: any = window; + // wnd.saveAs(blob, clean.title + '-' + new Date().getTime() + '.json'); }); } diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 5b0576b729c..62403efae7c 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -1,11 +1,13 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; +import _ from 'lodash'; +import config from 'app/core/config'; import {DashboardExporter} from '../exporter'; describe.only('given dashboard with repeated panels', function() { var dash, exported; - beforeEach((done) => { + beforeEach(done => { dash = { rows: [], templating: { list: [] } @@ -19,7 +21,7 @@ describe.only('given dashboard with repeated panels', function() { dash.rows.push({ repeat: 'test', panels: [ - {id: 2, repeat: 'apps', datasource: 'gfdb'}, + {id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'}, {id: 2, repeat: null, repeatPanelId: 2}, ] }); @@ -31,19 +33,23 @@ describe.only('given dashboard with repeated panels', function() { var datasourceSrvStub = { get: sinon.stub().returns(Promise.resolve({ name: 'gfdb', - meta: {id: "testdb"} + meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"} })) }; + config.panels['graph'] = { + id: "graph", + name: "Graph", + info: {version: "1.1.0"} + }; + var exporter = new DashboardExporter(datasourceSrvStub); exporter.makeExportable(dash).then(clean => { exported = clean; done(); - console.log('done'); }); }); - it('exported dashboard should not contain repeated panels', function() { expect(exported.rows[0].panels.length).to.be(1); }); @@ -63,5 +69,20 @@ describe.only('given dashboard with repeated panels', function() { expect(exported.__inputs[0].type).to.be("datasource"); }); + it('should add datasource to required', function() { + var require = _.findWhere(exported.__requires, {name: 'TestDB'}); + expect(require.name).to.be("TestDB"); + expect(require.id).to.be("testdb"); + expect(require.type).to.be("datasource"); + expect(require.version).to.be("1.2.1"); + }); + + it('should add panel to required', function() { + var require = _.findWhere(exported.__requires, {name: 'Graph'}); + expect(require.name).to.be("Graph"); + expect(require.id).to.be("graph"); + expect(require.version).to.be("1.1.0"); + }); + }); From 2c7447eaca6ff14bb2413265c7e68efc0d27fd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 May 2016 16:18:52 +0200 Subject: [PATCH 08/29] feat(): started work on new import system --- public/app/core/components/search/search.html | 15 +++-- public/app/core/core.ts | 1 - public/app/core/directives/dash_upload.js | 46 ------------- public/app/features/dashboard/all.js | 1 + public/app/features/dashboard/upload.ts | 65 +++++++++++++++++++ 5 files changed, 76 insertions(+), 52 deletions(-) delete mode 100644 public/app/core/directives/dash_upload.js create mode 100644 public/app/features/dashboard/upload.ts diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 35c4431d80c..953a769f7ee 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -64,12 +64,17 @@
- - - Import - + +
+ + +
+
diff --git a/public/app/core/core.ts b/public/app/core/core.ts index abebb5ce560..7c4ebc3d5ed 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -5,7 +5,6 @@ import "./directives/annotation_tooltip"; import "./directives/dash_class"; import "./directives/confirm_click"; import "./directives/dash_edit_link"; -import "./directives/dash_upload"; import "./directives/dropdown_typeahead"; import "./directives/grafana_version_check"; import "./directives/metric_segment"; diff --git a/public/app/core/directives/dash_upload.js b/public/app/core/directives/dash_upload.js deleted file mode 100644 index b03bc201e83..00000000000 --- a/public/app/core/directives/dash_upload.js +++ /dev/null @@ -1,46 +0,0 @@ -define([ - '../core_module', - 'app/core/utils/kbn', -], -function (coreModule, kbn) { - 'use strict'; - - coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) { - return { - restrict: 'A', - link: function(scope) { - function file_selected(evt) { - var files = evt.target.files; // FileList object - var readerOnload = function() { - return function(e) { - scope.$apply(function() { - try { - window.grafanaImportDashboard = JSON.parse(e.target.result); - } catch (err) { - console.log(err); - scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]); - return; - } - var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); - window.grafanaImportDashboard.id = null; - $location.path('/dashboard-import/' + title); - }); - }; - }; - for (var i = 0, f; f = files[i]; i++) { - var reader = new FileReader(); - reader.onload = (readerOnload)(f); - reader.readAsText(f); - } - } - // Check for the various File API support. - if (window.File && window.FileReader && window.FileList && window.Blob) { - // Something - document.getElementById('dashupload').addEventListener('change', file_selected, false); - } else { - alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); - } - } - }; - }); -}); diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 9d370921332..926288a6e71 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -16,4 +16,5 @@ define([ './graphiteImportCtrl', './importCtrl', './impression_store', + './upload', ], function () {}); diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts new file mode 100644 index 00000000000..90e811a43c1 --- /dev/null +++ b/public/app/features/dashboard/upload.ts @@ -0,0 +1,65 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; + +var wnd: any = window; + +class DashboardImporter { + + prepareForImport(dash) { + dash.id = null; + return Promise.resolve(dash); + } + +} + + +/** @ngInject */ +function uploadDashboardDirective(timer, alertSrv, $location) { + return { + restrict: 'A', + link: function(scope) { + function file_selected(evt) { + var files = evt.target.files; // FileList object + var readerOnload = function() { + return function(e) { + var dash; + try { + dash = JSON.parse(e.target.result); + } catch (err) { + console.log(err); + scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]); + return; + } + + var importer = new DashboardImporter(); + importer.prepareForImport(dash).then(modified => { + wnd.grafanaImportDashboard = modified; + var title = kbn.slugifyForUrl(dash.title); + + scope.$apply(function() { + $location.path('/dashboard-import/' + title); + }); + }); + }; + }; + + for (var i = 0, f; f = files[i]; i++) { + var reader = new FileReader(); + reader.onload = readerOnload(); + reader.readAsText(f); + } + } + // Check for the various File API support. + if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) { + // Something + document.getElementById('dashupload').addEventListener('change', file_selected, false); + } else { + alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); + } + } + }; +} + +coreModule.directive('dashUpload', uploadDashboardDirective); From 0d3e06e68add4b19cb53d4ae371bebc9acb154fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 12 May 2016 14:45:32 +0200 Subject: [PATCH 09/29] feat(wizard): merged wizard poc --- .../components/wizard}/wizard.html | 0 .../components/wizard}/wizard.ts | 20 +++++- public/app/core/core.ts | 2 + public/app/features/dashboard/upload.ts | 16 ++++- .../app/features/plugins/plugin_edit_ctrl.ts | 65 ++++--------------- 5 files changed, 46 insertions(+), 57 deletions(-) rename public/app/{features/plugins/partials => core/components/wizard}/wizard.html (100%) rename public/app/{features/plugins => core/components/wizard}/wizard.ts (64%) diff --git a/public/app/features/plugins/partials/wizard.html b/public/app/core/components/wizard/wizard.html similarity index 100% rename from public/app/features/plugins/partials/wizard.html rename to public/app/core/components/wizard/wizard.html diff --git a/public/app/features/plugins/wizard.ts b/public/app/core/components/wizard/wizard.ts similarity index 64% rename from public/app/features/plugins/wizard.ts rename to public/app/core/components/wizard/wizard.ts index 741b0f01b3e..943998f2e10 100644 --- a/public/app/features/plugins/wizard.ts +++ b/public/app/core/components/wizard/wizard.ts @@ -1,4 +1,4 @@ -/// +/// import config from 'app/core/config'; import _ from 'lodash'; @@ -23,6 +23,8 @@ export class WizardStep { export class WizardFlow { name: string; steps: WizardStep[]; + reject: any; + fulfill: any; constructor(name) { this.name = name; @@ -36,11 +38,25 @@ export class WizardFlow { }); } + next(index) { + var step = this.steps[0]; + + return step.fn().then(() => { + if (this.steps.length === index+1) { + return; + } + + return this.next(index+1); + }); + } + start() { appEvents.emit('show-modal', { - src: 'public/app/features/plugins/partials/wizard.html', + src: 'public/app/core/components/wizard/wizard.html', model: this }); + + return this.next(0); } } diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 7c4ebc3d5ed..4db7c7ad650 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -32,6 +32,7 @@ import {Emitter} from './utils/emitter'; import {layoutSelector} from './components/layout_selector/layout_selector'; import {switchDirective} from './components/switch'; import {dashboardSelector} from './components/dashboard_selector'; +import {WizardFlow} from './components/wizard/wizard'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -55,4 +56,5 @@ export { Emitter, appEvents, dashboardSelector, + WizardFlow, }; diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 90e811a43c1..35095fb6cb9 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -3,15 +3,27 @@ import kbn from 'app/core/utils/kbn'; import coreModule from 'app/core/core_module'; +import {WizardFlow} from 'app/core/core'; + var wnd: any = window; class DashboardImporter { prepareForImport(dash) { dash.id = null; - return Promise.resolve(dash); - } + var wizard = new WizardFlow('Import Dashboard'); + + wizard.addStep("Importing dashboard", function() { + return new Promise(done => { + setTimeout(done, 2000); + }); + }); + + return wizard.start().then(() => { + return dash; + }); + } } diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 782c324a22b..a10b5eb6e7a 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -4,8 +4,6 @@ import angular from 'angular'; import _ from 'lodash'; import appEvents from 'app/core/app_events'; -import {WizardFlow} from './wizard'; - export class PluginEditCtrl { model: any; pluginIcon: string; @@ -83,58 +81,19 @@ export class PluginEditCtrl { } update() { - var wizard = new WizardFlow("Application Setup"); - - wizard.addStep("Validating form", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); + this.preUpdateHook().then(() => { + var updateCmd = _.extend({ + enabled: this.model.enabled, + pinned: this.model.pinned, + jsonData: this.model.jsonData, + secureJsonData: this.model.secureJsonData, + }, {}); + return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd); + }) + .then(this.postUpdateHook) + .then((res) => { + window.location.href = window.location.href; }); - - wizard.addStep("Saving application config", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); - }); - - wizard.addStep("Validing key", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); - }); - - wizard.addStep("Adding Raintank metric data source", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); - }); - - wizard.addStep("Adding Raintank event data source", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); - }); - - wizard.addStep("Importing worldPing dashboards", () => { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }); - }); - - wizard.start(); - // this.preUpdateHook().then(() => { - // var updateCmd = _.extend({ - // enabled: this.model.enabled, - // pinned: this.model.pinned, - // jsonData: this.model.jsonData, - // secureJsonData: this.model.secureJsonData, - // }, {}); - // return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd); - // }) - // .then(this.postUpdateHook) - // .then((res) => { - // window.location.href = window.location.href; - // }); } importDashboards() { From ca8df67947f7ed4cb37c61a433c8c424d1ee8426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 May 2016 11:26:02 +0200 Subject: [PATCH 10/29] feat(import): things are starting to work --- .floo | 3 + .flooignore | 9 +++ .jscs.json | 2 +- .../dash_importer/dash_importer.html | 76 ++++++++++++++++++ .../components/dash_importer/dash_importer.ts | 77 +++++++++++++++++++ public/app/core/components/search/search.html | 13 ++-- public/app/core/components/search/search.ts | 5 ++ public/app/core/components/wizard/wizard.html | 24 +++--- public/app/core/components/wizard/wizard.ts | 34 +++++--- public/app/core/services/util_srv.ts | 1 + public/app/features/dashboard/upload.ts | 48 ++++-------- public/sass/_variables.dark.scss | 2 +- public/sass/utils/_validation.scss | 2 + 13 files changed, 230 insertions(+), 66 deletions(-) create mode 100644 .floo create mode 100644 .flooignore create mode 100644 public/app/core/components/dash_importer/dash_importer.html create mode 100644 public/app/core/components/dash_importer/dash_importer.ts diff --git a/.floo b/.floo new file mode 100644 index 00000000000..1201c5e93b9 --- /dev/null +++ b/.floo @@ -0,0 +1,3 @@ +{ + "url": "https://floobits.com/raintank/grafana" +} \ No newline at end of file diff --git a/.flooignore b/.flooignore new file mode 100644 index 00000000000..b300a62c4da --- /dev/null +++ b/.flooignore @@ -0,0 +1,9 @@ +#* +*.o +*.pyc +*.pyo +*~ +extern/ +node_modules/ +tmp +vendor/ \ No newline at end of file diff --git a/.jscs.json b/.jscs.json index dcf694dcc63..8fdad332de5 100644 --- a/.jscs.json +++ b/.jscs.json @@ -10,4 +10,4 @@ "disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideParentheses": true, "validateIndentation": 2 -} \ No newline at end of file +} diff --git a/public/app/core/components/dash_importer/dash_importer.html b/public/app/core/components/dash_importer/dash_importer.html new file mode 100644 index 00000000000..8b4e649dd50 --- /dev/null +++ b/public/app/core/components/dash_importer/dash_importer.html @@ -0,0 +1,76 @@ + + diff --git a/public/app/core/components/dash_importer/dash_importer.ts b/public/app/core/components/dash_importer/dash_importer.ts new file mode 100644 index 00000000000..2f04640ffc2 --- /dev/null +++ b/public/app/core/components/dash_importer/dash_importer.ts @@ -0,0 +1,77 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; + +import appEvents from 'app/core/app_events'; +import {WizardFlow} from 'app/core/core'; + +var wnd: any = window; + +export class DashImporter { + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + dismiss: any; + + constructor(private backendSrv, private $location) { + } + + onUpload(dash) { + this.dash = dash; + this.dash.id = null; + + this.backendSrv.saveDashboard(this.dash, {overwrite: false}).then(res => { + + }).catch(err => { + if (err.data.status === 'name-exists') { + err.isHandled = true; + this.step = 2; + this.nameExists = true; + } + console.log(err); + }); + } + + titleChanged() { + this.backendSrv.search({query: this.dash.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.dash.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } + + saveDashboard() { + return this.backendSrv.saveDashboard(this.dash, {overwrite: true}).then(res => { + this.$location.url('dashboard/db/' + res.slug); + this.dismiss(); + }); + } + + loadJsonText() { + try { + this.parseError = ''; + var dash = JSON.parse(this.jsonText); + this.onUpload(dash); + } catch (err) { + console.log(err); + this.parseError = err.message; + return; + } + } + + run() { + this.step = 0; + + appEvents.emit('show-modal', { + src: 'public/app/core/components/dash_importer/dash_importer.html', + model: this + }); + } +} diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 953a769f7ee..ddfe22215a9 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -67,14 +67,11 @@ Create New -
- - -
+ -
+
diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index e296acf56e1..30ad9792c31 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -5,6 +5,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from '../../core_module'; +import {DashImporter} from '../dash_importer/dash_importer'; export class SearchCtrl { isOpen: boolean; @@ -151,6 +152,10 @@ export class SearchCtrl { newDashboard() { this.$location.url('dashboard/new'); }; + + import() { + new DashImporter(this.backendSrv, this.$location).run(); + } } export function searchDirective() { diff --git a/public/app/core/components/wizard/wizard.html b/public/app/core/components/wizard/wizard.html index 4a532e81a0f..9d3f680649a 100644 --- a/public/app/core/components/wizard/wizard.html +++ b/public/app/core/components/wizard/wizard.html @@ -11,19 +11,21 @@ diff --git a/public/app/core/components/wizard/wizard.ts b/public/app/core/components/wizard/wizard.ts index 943998f2e10..2ae38cf9e03 100644 --- a/public/app/core/components/wizard/wizard.ts +++ b/public/app/core/components/wizard/wizard.ts @@ -8,40 +8,50 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; export class WizardSrv { - /** @ngInject */ constructor() { } - } -export class WizardStep { +export interface WizardStep { name: string; - fn: any; + type: string; + process: any; +} + +export class SelectOptionStep { + type: string; + name: string; + fulfill: any; + + constructor() { + this.type = 'select'; + } + + process() { + return new Promise((fulfill, reject) => { + + }); + } } export class WizardFlow { name: string; steps: WizardStep[]; - reject: any; - fulfill: any; constructor(name) { this.name = name; this.steps = []; } - addStep(name, stepFn) { - this.steps.push({ - name: name, - fn: stepFn - }); + addStep(step) { + this.steps.push(step); } next(index) { var step = this.steps[0]; - return step.fn().then(() => { + return step.process().then(() => { if (this.steps.length === index+1) { return; } diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 0f15a65a0dc..595962cd3c6 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -34,6 +34,7 @@ export class UtilSrv { Promise.resolve(modal).then(function(modalEl) { modalEl.modal('show'); + options.scope.model.dismiss = options.scope.dismiss; }); } } diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 35095fb6cb9..ddc4005546c 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -3,34 +3,22 @@ import kbn from 'app/core/utils/kbn'; import coreModule from 'app/core/core_module'; -import {WizardFlow} from 'app/core/core'; - -var wnd: any = window; - -class DashboardImporter { - - prepareForImport(dash) { - dash.id = null; - - var wizard = new WizardFlow('Import Dashboard'); - - wizard.addStep("Importing dashboard", function() { - return new Promise(done => { - setTimeout(done, 2000); - }); - }); - - return wizard.start().then(() => { - return dash; - }); - } -} - +var template = ` + + +`; /** @ngInject */ function uploadDashboardDirective(timer, alertSrv, $location) { return { - restrict: 'A', + restrict: 'E', + template: template, + scope: { + onUpload: '&', + }, link: function(scope) { function file_selected(evt) { var files = evt.target.files; // FileList object @@ -45,15 +33,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) { return; } - var importer = new DashboardImporter(); - importer.prepareForImport(dash).then(modified => { - wnd.grafanaImportDashboard = modified; - var title = kbn.slugifyForUrl(dash.title); - - scope.$apply(function() { - $location.path('/dashboard-import/' + title); - }); - }); + scope.onUpload({dash: dash}); }; }; @@ -63,6 +43,8 @@ function uploadDashboardDirective(timer, alertSrv, $location) { reader.readAsText(f); } } + + var wnd: any = window; // Check for the various File API support. if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) { // Something diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index fed3d805b82..29e3cce1ee5 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -235,7 +235,7 @@ $paginationActiveBackground: $blue; $state-warning-text: darken(#c09853, 10%); $state-warning-bg: $brand-warning; -$errorText: #b94a48; +$errorText: #E84D4D; $errorBackground: $btn-danger-bg; $successText: #468847; diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index 1145212707e..c0dd44d59a0 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,8 +1,10 @@ input[type=text].ng-dirty.ng-invalid { } +input.validation-error, input.ng-dirty.ng-invalid { box-shadow: inset 0 0px 5px $red; } + From d9d46096dd5c41600f96f9f32e29e53b47083f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 13 May 2016 17:39:22 +0200 Subject: [PATCH 11/29] feat(import): lots of work on dashboard import --- .../dash_importer/dash_importer.html | 76 ----------- .../components/dash_importer/dash_importer.ts | 77 ------------ public/app/core/components/search/search.html | 8 +- public/app/core/components/search/search.ts | 10 +- public/app/core/directives/dash_edit_link.js | 29 ++--- public/app/core/services/util_srv.ts | 2 +- public/app/features/dashboard/all.js | 1 + public/app/features/dashboard/exporter.ts | 1 + .../app/features/dashboard/import/import.html | 96 ++++++++++++++ .../app/features/dashboard/import/import.ts | 119 ++++++++++++++++++ public/app/features/dashboard/upload.ts | 4 +- public/sass/_variables.dark.scss | 4 +- public/vendor/angular-other/angular-strap.js | 11 +- 13 files changed, 247 insertions(+), 191 deletions(-) delete mode 100644 public/app/core/components/dash_importer/dash_importer.html delete mode 100644 public/app/core/components/dash_importer/dash_importer.ts create mode 100644 public/app/features/dashboard/import/import.html create mode 100644 public/app/features/dashboard/import/import.ts diff --git a/public/app/core/components/dash_importer/dash_importer.html b/public/app/core/components/dash_importer/dash_importer.html deleted file mode 100644 index 8b4e649dd50..00000000000 --- a/public/app/core/components/dash_importer/dash_importer.html +++ /dev/null @@ -1,76 +0,0 @@ - - diff --git a/public/app/core/components/dash_importer/dash_importer.ts b/public/app/core/components/dash_importer/dash_importer.ts deleted file mode 100644 index 2f04640ffc2..00000000000 --- a/public/app/core/components/dash_importer/dash_importer.ts +++ /dev/null @@ -1,77 +0,0 @@ -/// - -import kbn from 'app/core/utils/kbn'; -import coreModule from 'app/core/core_module'; - -import appEvents from 'app/core/app_events'; -import {WizardFlow} from 'app/core/core'; - -var wnd: any = window; - -export class DashImporter { - step: number; - jsonText: string; - parseError: string; - nameExists: boolean; - dash: any; - dismiss: any; - - constructor(private backendSrv, private $location) { - } - - onUpload(dash) { - this.dash = dash; - this.dash.id = null; - - this.backendSrv.saveDashboard(this.dash, {overwrite: false}).then(res => { - - }).catch(err => { - if (err.data.status === 'name-exists') { - err.isHandled = true; - this.step = 2; - this.nameExists = true; - } - console.log(err); - }); - } - - titleChanged() { - this.backendSrv.search({query: this.dash.title}).then(res => { - this.nameExists = false; - for (let hit of res) { - if (this.dash.title === hit.title) { - this.nameExists = true; - break; - } - } - }); - } - - saveDashboard() { - return this.backendSrv.saveDashboard(this.dash, {overwrite: true}).then(res => { - this.$location.url('dashboard/db/' + res.slug); - this.dismiss(); - }); - } - - loadJsonText() { - try { - this.parseError = ''; - var dash = JSON.parse(this.jsonText); - this.onUpload(dash); - } catch (err) { - console.log(err); - this.parseError = err.message; - return; - } - } - - run() { - this.step = 0; - - appEvents.emit('show-modal', { - src: 'public/app/core/components/dash_importer/dash_importer.html', - model: this - }); - } -} diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index ddfe22215a9..72ff0d15f83 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -62,15 +62,15 @@ diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 30ad9792c31..9da95278c57 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -5,7 +5,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from '../../core_module'; -import {DashImporter} from '../dash_importer/dash_importer'; +import appEvents from 'app/core/app_events'; export class SearchCtrl { isOpen: boolean; @@ -149,12 +149,10 @@ export class SearchCtrl { this.searchDashboards(); }; - newDashboard() { - this.$location.url('dashboard/new'); - }; - import() { - new DashImporter(this.backendSrv, this.$location).run(); + appEvents.emit('show-modal', { + templateHtml: '', + }); } } diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index 23855925541..eb0fe453741 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -6,27 +6,12 @@ function ($, coreModule) { 'use strict'; var editViewMap = { - 'settings': { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" }, - 'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" }, - 'templating': { src: 'public/app/features/templating/partials/editor.html', title: "Templating" } + 'settings': { src: 'public/app/features/dashboard/partials/settings.html'}, + 'annotations': { src: 'public/app/features/annotations/partials/editor.html'}, + 'templating': { src: 'public/app/features/templating/partials/editor.html'}, + 'import': { src: '' } }; - coreModule.default.directive('dashEditorLink', function($timeout) { - return { - restrict: 'A', - link: function(scope, elem, attrs) { - var partial = attrs.dashEditorLink; - - elem.bind('click',function() { - $timeout(function() { - var editorScope = attrs.editorScope === 'isolated' ? null : scope; - scope.appEvent('show-dash-editor', { src: partial, scope: editorScope }); - }); - }); - } - }; - }); - coreModule.default.directive('dashEditorView', function($compile, $location) { return { restrict: 'A', @@ -72,8 +57,10 @@ function ($, coreModule) { } }; - var src = "'" + payload.src + "'"; - var view = $('
'); + var view = payload.src; + if (view.indexOf('.html') > 0) { + view = $('
'); + } elem.append(view); $compile(elem.contents())(editorScope); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 595962cd3c6..538527b5fda 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -26,6 +26,7 @@ export class UtilSrv { var modal = this.$modal({ modalClass: options.modalClass, template: options.src, + templateHtml: options.templateHtml, persist: false, show: false, scope: options.scope, @@ -34,7 +35,6 @@ export class UtilSrv { Promise.resolve(modal).then(function(modalEl) { modalEl.modal('show'); - options.scope.model.dismiss = options.scope.dismiss; }); } } diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 926288a6e71..51a2b806e19 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -17,4 +17,5 @@ define([ './importCtrl', './impression_store', './upload', + './import/import', ], function () {}); diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/exporter.ts index 9a8d5bae5b1..63067502605 100644 --- a/public/app/features/dashboard/exporter.ts +++ b/public/app/features/dashboard/exporter.ts @@ -29,6 +29,7 @@ export class DashboardExporter { name: refName, type: 'datasource', pluginId: ds.meta.id, + pluginName: ds.meta.name, }; panel.datasource = '${' + refName +'}'; diff --git a/public/app/features/dashboard/import/import.html b/public/app/features/dashboard/import/import.html new file mode 100644 index 00000000000..8a3a276ce53 --- /dev/null +++ b/public/app/features/dashboard/import/import.html @@ -0,0 +1,96 @@ + + diff --git a/public/app/features/dashboard/import/import.ts b/public/app/features/dashboard/import/import.ts new file mode 100644 index 00000000000..852a927ded8 --- /dev/null +++ b/public/app/features/dashboard/import/import.ts @@ -0,0 +1,119 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; +import _ from 'lodash'; + +export class DashImportCtrl { + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + dismiss: any; + inputs: any[]; + inputsValid: boolean; + + /** @ngInject */ + constructor(private backendSrv, private $location, private $scope) { + this.step = 1; + this.nameExists = false; + } + + onUpload(dash) { + this.dash = dash; + this.dash.id = null; + this.step = 2; + this.inputs = []; + + if (this.dash.__inputs) { + for (let input of this.dash.__inputs) { + var inputModel = { + name: input.name, + type: input.type, + options: [] + }; + + if (input.type === 'datasource') { + this.setDatasourceOptions(input, inputModel); + } + + this.inputs.push(inputModel); + } + } + + this.inputsValid = this.inputs.length === 0; + this.titleChanged(); + } + + setDatasourceOptions(input, inputModel) { + var sources = _.filter(config.datasources, val => { + return val.type === input.pluginId; + }); + + if (sources.length === 0) { + inputModel.error = "No data sources of type " + input.pluginName + " found"; + } else { + inputModel.info = "Select a " + input.pluginName + " data source"; + } + + inputModel.options = sources.map(val => { + return {text: val.name, value: val.name}; + }); + } + + inputOptionChanged() { + this.inputsValid = true; + for (let input of this.inputs) { + if (!input.value) { + this.inputsValid = false; + } + } + } + + titleChanged() { + this.backendSrv.search({query: this.dash.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.dash.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } + + saveDashboard() { + return this.backendSrv.saveDashboard(this.dash, {overwrite: true}).then(res => { + this.$location.url('dashboard/db/' + res.slug); + this.dismiss(); + }); + } + + loadJsonText() { + try { + this.parseError = ''; + var dash = JSON.parse(this.jsonText); + this.onUpload(dash); + } catch (err) { + console.log(err); + this.parseError = err.message; + return; + } + } + +} + +export function dashImportDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/import/import.html', + controller: DashImportCtrl, + bindToController: true, + controllerAs: 'ctrl', + }; +} + +coreModule.directive('dashImport', dashImportDirective); diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index ddc4005546c..57be7b8fd11 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -33,7 +33,9 @@ function uploadDashboardDirective(timer, alertSrv, $location) { return; } - scope.onUpload({dash: dash}); + scope.$apply(function() { + scope.onUpload({dash: dash}); + }); }; }; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 29e3cce1ee5..d977689f553 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -232,13 +232,13 @@ $paginationActiveBackground: $blue; // Form states and alerts // ------------------------- -$state-warning-text: darken(#c09853, 10%); +$state-warning-text: $warn; $state-warning-bg: $brand-warning; $errorText: #E84D4D; $errorBackground: $btn-danger-bg; -$successText: #468847; +$successText: #12D95A; $successBackground: $btn-success-bg; $infoText: $blue-dark; diff --git a/public/vendor/angular-other/angular-strap.js b/public/vendor/angular-other/angular-strap.js index d9721bda038..3670f3f8ac4 100644 --- a/public/vendor/angular-other/angular-strap.js +++ b/public/vendor/angular-other/angular-strap.js @@ -25,11 +25,16 @@ angular.module('$strap.directives').factory('$modal', [ function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) { var ModalFactory = function ModalFactory(config) { function Modal(config) { - var options = angular.extend({ show: true }, $strapConfig.modal, config), scope = options.scope ? options.scope : $rootScope.$new(), templateUrl = options.template; - return $q.when($templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) { + var options = angular.extend({ show: true }, $strapConfig.modal, config); + var scope = options.scope ? options.scope : $rootScope.$new() + var templateUrl = options.template; + return $q.when(options.templateHtml || $templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) { return res.data; })).then(function onSuccess(template) { - var id = templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id; + var id = scope.$id; + if (templateUrl) { + id += templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-'); + } // grafana change, removed fade var $modal = $('').attr('id', id).html(template); if (options.modalClass) From 7cd663bbe819feb819bb155ad434d67130f292bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 May 2016 10:00:43 +0200 Subject: [PATCH 12/29] feat(import): more import work --- pkg/api/dtos/plugins.go | 6 +++- pkg/api/plugins.go | 3 +- pkg/plugins/dashboard_importer.go | 29 ++++++++-------- pkg/plugins/dashboards.go | 33 +++++++++++-------- public/app/core/components/search/search.html | 2 +- public/app/core/components/search/search.ts | 5 --- public/app/core/directives/dash_edit_link.js | 17 +++++++++- public/app/core/services/datasource_srv.js | 10 +++--- public/app/core/services/util_srv.ts | 3 +- .../app/features/dashboard/import/import.html | 2 +- .../app/features/dashboard/import/import.ts | 23 ++++++++++--- .../plugins/import_list/import_list.html | 12 +++---- .../plugins/import_list/import_list.ts | 6 ++-- 13 files changed, 92 insertions(+), 59 deletions(-) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index fccb7c36849..70e732424ab 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -1,6 +1,9 @@ package dtos -import "github.com/grafana/grafana/pkg/plugins" +import ( + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/plugins" +) type PluginSetting struct { Name string `json:"name"` @@ -50,5 +53,6 @@ type ImportDashboardCommand struct { PluginId string `json:"pluginId"` Path string `json:"path"` Overwrite bool `json:"overwrite"` + Dashboard *simplejson.Json `json:"dashboard"` Inputs []plugins.ImportDashboardInput `json:"inputs"` } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 7d6d5906913..9d25b9c331e 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -168,10 +168,11 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Path: apiCmd.Path, Inputs: apiCmd.Inputs, Overwrite: apiCmd.Overwrite, + Dashboard: apiCmd.Dashboard, } if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to install dashboard", err) + return ApiError(500, "Failed to import dashboard", err) } return Json(200, cmd.Result) diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 4d2757b9a0e..aa489fcdaaf 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -11,6 +11,7 @@ import ( ) type ImportDashboardCommand struct { + Dashboard *simplejson.Json Path string Inputs []ImportDashboardInput Overwrite bool @@ -41,17 +42,15 @@ func init() { } func ImportDashboard(cmd *ImportDashboardCommand) error { - plugin, exists := Plugins[cmd.PluginId] - - if !exists { - return PluginNotFoundError{cmd.PluginId} - } - var dashboard *m.Dashboard var err error - if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { - return err + if cmd.PluginId != "" { + if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil { + return err + } + } else { + dashboard = m.NewDashboardFromJson(cmd.Dashboard) } evaluator := &DashTemplateEvaluator{ @@ -76,13 +75,13 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { } cmd.Result = &PluginDashboardInfoDTO{ - PluginId: cmd.PluginId, - Title: dashboard.Title, - Path: cmd.Path, - Revision: dashboard.GetString("revision", "1.0"), - InstalledUri: "db/" + saveCmd.Result.Slug, - InstalledRevision: dashboard.GetString("revision", "1.0"), - Installed: true, + PluginId: cmd.PluginId, + Title: dashboard.Title, + Path: cmd.Path, + Revision: dashboard.Data.Get("revision").MustInt64(1), + ImportedUri: "db/" + saveCmd.Result.Slug, + ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), + Imported: true, } return nil diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 932196a42a9..1a160fe6632 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -10,14 +10,14 @@ import ( ) type PluginDashboardInfoDTO struct { - PluginId string `json:"pluginId"` - Title string `json:"title"` - Installed bool `json:"installed"` - InstalledUri string `json:"installedUri"` - InstalledRevision string `json:"installedRevision"` - Revision string `json:"revision"` - Description string `json:"description"` - Path string `json:"path"` + PluginId string `json:"pluginId"` + Title string `json:"title"` + Imported bool `json:"imported"` + ImportedUri string `json:"importedUri"` + ImportedRevision int64 `json:"importedRevision"` + Revision int64 `json:"revision"` + Description string `json:"description"` + Path string `json:"path"` } func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { @@ -42,7 +42,12 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT return result, nil } -func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) { +func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) { + plugin, exists := Plugins[pluginId] + + if !exists { + return nil, PluginNotFoundError{pluginId} + } dashboardFilePath := filepath.Join(plugin.PluginDir, path) reader, err := os.Open(dashboardFilePath) @@ -66,14 +71,14 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl var dashboard *m.Dashboard var err error - if dashboard, err = loadPluginDashboard(plugin, path); err != nil { + if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil { return nil, err } res.Path = path res.PluginId = plugin.Id res.Title = dashboard.Title - res.Revision = dashboard.GetString("revision", "1.0") + res.Revision = dashboard.Data.Get("revision").MustInt64(1) query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug} @@ -82,9 +87,9 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl return nil, err } } else { - res.Installed = true - res.InstalledUri = "db/" + query.Result.Slug - res.InstalledRevision = query.Result.GetString("revision", "1.0") + res.Imported = true + res.ImportedUri = "db/" + query.Result.Slug + res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1) } return res, nil diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 72ff0d15f83..6344a26c886 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -67,7 +67,7 @@ Create New - + Import diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 9da95278c57..a581afe3fd3 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -149,11 +149,6 @@ export class SearchCtrl { this.searchDashboards(); }; - import() { - appEvents.emit('show-modal', { - templateHtml: '', - }); - } } export function searchDirective() { diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index eb0fe453741..7486d0ada18 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -12,7 +12,7 @@ function ($, coreModule) { 'import': { src: '' } }; - coreModule.default.directive('dashEditorView', function($compile, $location) { + coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) { return { restrict: 'A', link: function(scope, elem) { @@ -57,6 +57,21 @@ function ($, coreModule) { } }; + if (editview === 'import') { + var modalScope = $rootScope.$new(); + modalScope.$on("$destroy", function() { + editorScope.dismiss(); + }); + + $rootScope.appEvent('show-modal', { + templateHtml: '', + scope: modalScope, + backdrop: 'static' + }); + + return; + } + var view = payload.src; if (view.indexOf('.html') > 0) { view = $('
'); diff --git a/public/app/core/services/datasource_srv.js b/public/app/core/services/datasource_srv.js index 32bc9a39725..a71b2bd3767 100644 --- a/public/app/core/services/datasource_srv.js +++ b/public/app/core/services/datasource_srv.js @@ -81,11 +81,11 @@ function (angular, _, coreModule, config) { _.each(config.datasources, function(value, key) { if (value.meta && value.meta.metrics) { - metricSources.push({ - value: key === config.defaultDatasource ? null : key, - name: key, - meta: value.meta, - }); + metricSources.push({value: key, name: key, meta: value.meta}); + + if (key === config.defaultDatasource) { + metricSources.push({value: null, name: 'default', meta: value.meta}); + } } }); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 538527b5fda..8ca7bf8be72 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -30,7 +30,8 @@ export class UtilSrv { persist: false, show: false, scope: options.scope, - keyboard: false + keyboard: false, + backdrop: options.backdrop }); Promise.resolve(modal).then(function(modalEl) { diff --git a/public/app/features/dashboard/import/import.html b/public/app/features/dashboard/import/import.html index 8a3a276ce53..7b60654dd50 100644 --- a/public/app/features/dashboard/import/import.html +++ b/public/app/features/dashboard/import/import.html @@ -84,7 +84,7 @@
- Cancel diff --git a/public/app/features/dashboard/import/import.ts b/public/app/features/dashboard/import/import.ts index 852a927ded8..dbf6b08c222 100644 --- a/public/app/features/dashboard/import/import.ts +++ b/public/app/features/dashboard/import/import.ts @@ -12,7 +12,6 @@ export class DashImportCtrl { parseError: string; nameExists: boolean; dash: any; - dismiss: any; inputs: any[]; inputsValid: boolean; @@ -33,6 +32,7 @@ export class DashImportCtrl { var inputModel = { name: input.name, type: input.type, + pluginId: input.pluginId, options: [] }; @@ -64,7 +64,7 @@ export class DashImportCtrl { }); } - inputOptionChanged() { + inputValueChanged() { this.inputsValid = true; for (let input of this.inputs) { if (!input.value) { @@ -86,9 +86,22 @@ export class DashImportCtrl { } saveDashboard() { - return this.backendSrv.saveDashboard(this.dash, {overwrite: true}).then(res => { - this.$location.url('dashboard/db/' + res.slug); - this.dismiss(); + var inputs = this.inputs.map(input => { + return { + name: input.name, + type: input.type, + pluginId: input.pluginId, + value: input.value + }; + }); + + return this.backendSrv.post('api/dashboards/import', { + dashboard: this.dash, + overwrite: true, + inputs: inputs + }).then(res => { + this.$location.url('dashboard/' + res.importedUri); + this.$scope.dismiss(); }); } diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index 86534f1adc7..746109970e0 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -6,27 +6,27 @@ - + {{dash.title}} - + {{dash.title}} v{{dash.revision}} -  (Imported v{{dash.installedRevision}}) +  (Imported v{{dash.importedRevision}}) - - - diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 35f9fc62cad..0c5006b09ba 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -61,15 +61,15 @@ export class DashImportListCtrl { } return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => { - this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); + this.$rootScope.appEvent('alert-success', ['Dashboard Imported', dash.title]); _.extend(dash, res); }); } remove(dash) { - this.backendSrv.delete('/api/dashboards/' + dash.installedUri).then(() => { + this.backendSrv.delete('/api/dashboards/' + dash.importedUri).then(() => { this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]); - dash.installed = false; + dash.imported = false; }); } } From ad7a1e15b4d0830c0589aad7bdc406f2339f9078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 May 2016 10:29:57 +0200 Subject: [PATCH 13/29] feat(export): began working on export modal --- pkg/plugins/dashboard_importer.go | 2 +- public/app/features/dashboard/all.js | 1 + .../features/dashboard/dashnav/dashnav.html | 5 ++ .../app/features/dashboard/dashnav/dashnav.ts | 10 ++- .../dashboard/export/export_modal.html | 74 +++++++++++++++++++ .../features/dashboard/export/export_modal.ts | 40 ++++++++++ .../dashboard/{ => export}/exporter.ts | 4 +- public/app/features/dashboard/keybindings.js | 4 - .../dashboard/specs/exporter_specs.ts | 2 +- public/app/partials/help_modal.html | 4 - 10 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 public/app/features/dashboard/export/export_modal.html create mode 100644 public/app/features/dashboard/export/export_modal.ts rename public/app/features/dashboard/{ => export}/exporter.ts (95%) diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index aa489fcdaaf..8f6998d344d 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -109,7 +109,7 @@ func (this *DashTemplateEvaluator) findInput(varName string, varType string) *Im func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { this.result = simplejson.New() this.variables = make(map[string]string) - this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`) + this.varRegex, _ = regexp.Compile(`(\$\{.+\})`) // check that we have all inputs we need for _, inputDef := range this.template.Get("__inputs").MustArray() { diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 51a2b806e19..a7b40b14b58 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -18,4 +18,5 @@ define([ './impression_store', './upload', './import/import', + './export/export_modal', ], function () {}); diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 9afd152d8aa..1a0c8a56e21 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -33,6 +33,11 @@ Snapshot sharing +
  • + + Export + +
  • diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index e9ef96408ad..81f2d9ea8c6 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -4,7 +4,7 @@ import _ from 'lodash'; import moment from 'moment'; import angular from 'angular'; -import {DashboardExporter} from '../exporter'; +import {DashboardExporter} from '../export/exporter'; export class DashNavCtrl { @@ -14,7 +14,6 @@ export class DashNavCtrl { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); $scope.onAppEvent('delete-dashboard', $scope.deleteDashboard); - $scope.onAppEvent('export-dashboard', $scope.snapshot); $scope.onAppEvent('quick-snapshot', $scope.quickSnapshot); $scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor; @@ -60,6 +59,12 @@ export class DashNavCtrl { $scope.shareDashboard(1); }; + $scope.shareExport = function() { + $scope.appEvent('show-modal', { + templateHtml: '', + }); + }; + $scope.openSearch = function() { $scope.appEvent('show-dash-search'); }; @@ -181,7 +186,6 @@ export class DashNavCtrl { $rootScope.$broadcast('refresh'); $timeout(function() { - $scope.exportDashboard(); $scope.dashboard.snapshot = false; $scope.appEvent('dashboard-snapshot-cleanup'); }, 1000); diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html new file mode 100644 index 00000000000..fe821887ca4 --- /dev/null +++ b/public/app/features/dashboard/export/export_modal.html @@ -0,0 +1,74 @@ + +
  • + diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts new file mode 100644 index 00000000000..3857d8a1330 --- /dev/null +++ b/public/app/features/dashboard/export/export_modal.ts @@ -0,0 +1,40 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; +import _ from 'lodash'; + +import {DashboardExporter} from './exporter'; + +export class DashExportCtrl { + dash: any; + exporter: DashboardExporter; + + /** @ngInject */ + constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) { + this.exporter = new DashboardExporter(datasourceSrv); + + var current = dashboardSrv.getCurrent().getSaveModelClone(); + + this.exporter.makeExportable(current).then(dash => { + $scope.$apply(() => { + this.dash = dash; + }); + }); + } + +} + +export function dashExportDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/export/export_modal.html', + controller: DashExportCtrl, + bindToController: true, + controllerAs: 'ctrl', + }; +} + +coreModule.directive('dashExportModal', dashExportDirective); diff --git a/public/app/features/dashboard/exporter.ts b/public/app/features/dashboard/export/exporter.ts similarity index 95% rename from public/app/features/dashboard/exporter.ts rename to public/app/features/dashboard/export/exporter.ts index 63067502605..0e73cbcf038 100644 --- a/public/app/features/dashboard/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -1,10 +1,10 @@ -/// +/// import config from 'app/core/config'; import angular from 'angular'; import _ from 'lodash'; -import {DynamicDashboardSrv} from './dynamic_dashboard_srv'; +import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; export class DashboardExporter { diff --git a/public/app/features/dashboard/keybindings.js b/public/app/features/dashboard/keybindings.js index b07dd2fd848..429e5f5d667 100644 --- a/public/app/features/dashboard/keybindings.js +++ b/public/app/features/dashboard/keybindings.js @@ -68,10 +68,6 @@ function(angular, $) { scope.appEvent('shift-time-forward', evt); }, { inputDisabled: true }); - keyboardManager.bind('ctrl+e', function(evt) { - scope.appEvent('export-dashboard', evt); - }, { inputDisabled: true }); - keyboardManager.bind('ctrl+i', function(evt) { scope.appEvent('quick-snapshot', evt); }, { inputDisabled: true }); diff --git a/public/app/features/dashboard/specs/exporter_specs.ts b/public/app/features/dashboard/specs/exporter_specs.ts index 62403efae7c..408d8cc5bb3 100644 --- a/public/app/features/dashboard/specs/exporter_specs.ts +++ b/public/app/features/dashboard/specs/exporter_specs.ts @@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co import _ from 'lodash'; import config from 'app/core/config'; -import {DashboardExporter} from '../exporter'; +import {DashboardExporter} from '../export/exporter'; describe.only('given dashboard with repeated panels', function() { var dash, exported; diff --git a/public/app/partials/help_modal.html b/public/app/partials/help_modal.html index 6a8c888bda3..e6601001433 100644 --- a/public/app/partials/help_modal.html +++ b/public/app/partials/help_modal.html @@ -40,10 +40,6 @@ CTRL+S Save dashboard - - CTRL+E - Export dashboard - CTRL+H Hide row controls From df50fa233255b88a28a8e17299f8160ca2d56e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 May 2016 11:17:11 +0200 Subject: [PATCH 14/29] feat(export): export dashboard modal --- docs/sources/reference/dashboard.md | 2 - public/app/features/dashboard/dashboardSrv.js | 1 - .../features/dashboard/dashnav/dashnav.html | 2 +- .../dashboard/export/export_modal.html | 50 ++++++++----------- .../features/dashboard/export/export_modal.ts | 13 +++++ .../app/features/dashboard/export/exporter.ts | 12 +++-- public/dashboards/home.json | 1 - public/dashboards/template_vars.json | 1 - 8 files changed, 44 insertions(+), 38 deletions(-) diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 93adf5cd789..831dbe3abdc 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -26,7 +26,6 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized { "id": null, "title": "New dashboard", - "originalTitle": "New dashboard", "tags": [], "style": "dark", "timezone": "browser", @@ -59,7 +58,6 @@ Each field in the dashboard JSON is explained below with its usage: | ---- | ----- | | **id** | unique dashboard id, an integer | | **title** | current title of dashboard | -| **originalTitle** | title of dashboard when saved for the first time | | **tags** | tags associated with dashboard, an array of strings | | **style** | theme of dashboard, i.e. `dark` or `light` | | **timezone** | timezone of dashboard, i.e. `utc` or `browser` | diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index 2e1cd1acbf0..8181af4566f 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -22,7 +22,6 @@ function (angular, $, _, moment) { this.id = data.id || null; this.title = data.title || 'No Title'; - this.originalTitle = this.title; this.tags = data.tags || []; this.style = data.style || "dark"; this.timezone = data.timezone || ''; diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 1a0c8a56e21..342612d12ce 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -35,7 +35,7 @@
  • - Export + Export for sharing
  • diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html index fe821887ca4..a4c40cc6889 100644 --- a/public/app/features/dashboard/export/export_modal.html +++ b/public/app/features/dashboard/export/export_modal.html @@ -12,10 +12,10 @@ -

    - Dashboard data sources -

    - -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    + + + + + + + + + + + +
    - + Cancel
    - diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 3857d8a1330..57af9d9caf8 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -1,6 +1,7 @@ /// import kbn from 'app/core/utils/kbn'; +import angular from 'angular'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import config from 'app/core/config'; @@ -25,6 +26,18 @@ export class DashExportCtrl { }); } + save() { + var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" }); + var wnd: any = window; + wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json'); + } + + saveJson() { + var html = angular.toJson(this.dash, true); + var uri = "data:application/json," + encodeURIComponent(html); + var newWindow = window.open(uri); + } + } export function dashExportDirective() { diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index 0e73cbcf038..d3b94d7fae7 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -15,6 +15,8 @@ export class DashboardExporter { var dynSrv = new DynamicDashboardSrv(); dynSrv.process(dash, {cleanUpOnly: true}); + dash.id = null; + var inputs = []; var requires = {}; var datasources = {}; @@ -63,10 +65,14 @@ export class DashboardExporter { return req; }); - dash["__inputs"] = inputs; - dash["__requires"] = requires; + // make inputs and requires a top thing + var newObj = {}; + newObj["__inputs"] = inputs; + newObj["__requires"] = requires; - return dash; + _.defaults(newObj, dash); + + return newObj; }).catch(err => { console.log('Export failed:', err); return {}; diff --git a/public/dashboards/home.json b/public/dashboards/home.json index c3ed1017bad..393cbc5865c 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -1,7 +1,6 @@ { "id": null, "title": "Home", - "originalTitle": "Home", "tags": [], "style": "dark", "timezone": "browser", diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json index 8ca81fbdff5..a3c25d00371 100644 --- a/public/dashboards/template_vars.json +++ b/public/dashboards/template_vars.json @@ -1,7 +1,6 @@ { "id": null, "title": "Templated Graphs Nested", - "originalTitle": "Templated Graphs Nested", "tags": [ "showcase", "templated" From 05d064ca8d56141f671249e1dd77aae307b85feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 May 2016 21:18:47 +0200 Subject: [PATCH 15/29] export(): moved to share modal --- .../features/dashboard/dashnav/dashnav.html | 8 +- .../app/features/dashboard/dashnav/dashnav.ts | 6 - .../dashboard/export/export_modal.html | 113 ++++++++---------- .../dashboard/partials/shareModal.html | 4 + .../app/features/dashboard/shareModalCtrl.js | 8 +- 5 files changed, 66 insertions(+), 73 deletions(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 342612d12ce..8a50c9d3bda 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -30,12 +30,12 @@
  • - Snapshot sharing + Snapshot
  • - - Export for sharing + + Export
  • @@ -49,8 +49,6 @@
  • Settings
  • Annotations
  • Templating
  • -
  • Export
  • -
  • View JSON
  • Make Editable
  • Save As...
  • Delete dashboard
  • diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 81f2d9ea8c6..ae7d81dfc58 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -59,12 +59,6 @@ export class DashNavCtrl { $scope.shareDashboard(1); }; - $scope.shareExport = function() { - $scope.appEvent('show-modal', { - templateHtml: '', - }); - }; - $scope.openSearch = function() { $scope.appEvent('show-dash-search'); }; diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html index a4c40cc6889..d2643eb934d 100644 --- a/public/app/features/dashboard/export/export_modal.html +++ b/public/app/features/dashboard/export/export_modal.html @@ -1,66 +1,59 @@ -