Quellcode durchsuchen

Merge branch 'feat-10064' of https://github.com/alexanderzobnin/grafana into develop

Torkel Ödegaard vor 8 Jahren
Ursprung
Commit
cc2349f6aa

+ 2 - 1
pkg/api/index.go

@@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 	if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
 	if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
 		data.NavTree = append(data.NavTree, &dtos.NavLink{
 		data.NavTree = append(data.NavTree, &dtos.NavLink{
 			Text: "Create",
 			Text: "Create",
+			Id:   "create",
 			Icon: "fa fa-fw fa-plus",
 			Icon: "fa fa-fw fa-plus",
 			Url:  "#",
 			Url:  "#",
 			Children: []*dtos.NavLink{
 			Children: []*dtos.NavLink{
 				{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
 				{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
 				{Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
 				{Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"},
-				{Text: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"},
+				{Text: "Import", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
 			},
 			},
 		})
 		})
 	}
 	}

+ 5 - 0
public/app/core/routes/routes.ts

@@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     reloadOnSearch: false,
     reloadOnSearch: false,
     pageClass: 'page-dashboard',
     pageClass: 'page-dashboard',
   })
   })
+  .when('/dashboard/import', {
+    templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html',
+    controller : 'DashboardImportCtrl',
+    controllerAs: 'ctrl',
+  })
   .when('/datasources', {
   .when('/datasources', {
     templateUrl: 'public/app/features/plugins/partials/ds_list.html',
     templateUrl: 'public/app/features/plugins/partials/ds_list.html',
     controller : 'DataSourcesCtrl',
     controller : 'DataSourcesCtrl',

+ 2 - 1
public/app/features/dashboard/all.ts

@@ -15,7 +15,6 @@ import './unsavedChangesSrv';
 import './unsaved_changes_modal';
 import './unsaved_changes_modal';
 import './timepicker/timepicker';
 import './timepicker/timepicker';
 import './upload';
 import './upload';
-import './import/dash_import';
 import './export/export_modal';
 import './export/export_modal';
 import './export_data/export_data_modal';
 import './export_data/export_data_modal';
 import './ad_hoc_filters';
 import './ad_hoc_filters';
@@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 
 
 import {DashboardListCtrl} from './dashboard_list_ctrl';
 import {DashboardListCtrl} from './dashboard_list_ctrl';
+import {DashboardImportCtrl} from './dashboard_import_ctrl';
 
 
 coreModule.controller('DashboardListCtrl', DashboardListCtrl);
 coreModule.controller('DashboardListCtrl', DashboardListCtrl);
+coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);

+ 163 - 0
public/app/features/dashboard/dashboard_import_ctrl.ts

@@ -0,0 +1,163 @@
+import _ from 'lodash';
+import config from 'app/core/config';
+
+export class DashboardImportCtrl {
+  navModel: any;
+  step: number;
+  jsonText: string;
+  parseError: string;
+  nameExists: boolean;
+  dash: any;
+  inputs: any[];
+  inputsValid: boolean;
+  gnetUrl: string;
+  gnetError: string;
+  gnetInfo: any;
+
+  /** @ngInject */
+  constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) {
+    this.navModel = navModelSrv.getNav('create', 'import');
+
+    this.step = 1;
+    this.nameExists = false;
+
+    // check gnetId in url
+    if ($routeParams.gnetId)  {
+      this.gnetUrl = $routeParams.gnetId ;
+      this.checkGnetDashboard();
+    }
+  }
+
+  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,
+          label: input.label,
+          info: input.description,
+          value: input.value,
+          type: input.type,
+          pluginId: input.pluginId,
+          options: []
+        };
+
+        if (input.type === 'datasource') {
+          this.setDatasourceOptions(input, inputModel);
+        } else if (!inputModel.info) {
+          inputModel.info = 'Specify a string constant';
+        }
+
+        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.info = "No data sources of type " + input.pluginName + " found";
+    } else if (!inputModel.info) {
+      inputModel.info = "Select a " + input.pluginName + " data source";
+    }
+
+    inputModel.options = sources.map(val => {
+      return {text: val.name, value: val.name};
+    });
+  }
+
+  inputValueChanged() {
+    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() {
+    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();
+    });
+  }
+
+  loadJsonText() {
+    try {
+      this.parseError = '';
+      var dash = JSON.parse(this.jsonText);
+      this.onUpload(dash);
+    } catch (err) {
+      console.log(err);
+      this.parseError = err.message;
+      return;
+    }
+  }
+
+  checkGnetDashboard() {
+    this.gnetError = '';
+
+    var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl);
+    var dashboardId;
+
+    if (match && match[1]) {
+      dashboardId = match[1];
+    } else if (match && match[2]) {
+      dashboardId = match[2];
+    } else {
+      this.gnetError = 'Could not find dashboard';
+    }
+
+    return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => {
+      this.gnetInfo = res;
+      // store reference to grafana.com
+      res.json.gnetId = res.id;
+      this.onUpload(res.json);
+    }).catch(err => {
+      err.isHandled = true;
+      this.gnetError = err.data.message || err;
+    });
+  }
+
+  back() {
+    this.gnetUrl = '';
+    this.step = 1;
+    this.gnetError = '';
+    this.gnetInfo = '';
+  }
+}

+ 125 - 0
public/app/features/dashboard/partials/dashboardImport.html

@@ -0,0 +1,125 @@
+<page-header model="ctrl.navModel"></page-header>
+
+<div class="page-container page-body" ng-cloak>
+  <div ng-if="ctrl.step === 1">
+
+    <form class="gf-form-group">
+      <dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
+    </form>
+
+    <h5 class="section-heading">Grafana.com Dashboard</h5>
+
+    <div class="gf-form-group">
+      <div class="gf-form gf-form--grow">
+        <input type="text" class="gf-form-input max-width-30" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.com dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
+      </div>
+      <div class="gf-form" ng-if="ctrl.gnetError">
+        <label class="gf-form-label text-warning">
+          <i class="fa fa-warning"></i>
+          {{ctrl.gnetError}}
+        </label>
+      </div>
+    </div>
+
+    <h5 class="section-heading">Or paste JSON</h5>
+
+    <div class="gf-form-group">
+      <div class="gf-form">
+        <textarea rows="10" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
+      </div>
+      <button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
+        <i class="fa fa-paste"></i>
+        Load
+      </button>
+      <span ng-if="ctrl.parseError" class="text-error p-l-1">
+        <i class="fa fa-warning"></i>
+        {{ctrl.parseError}}
+      </span>
+    </div>
+  </div>
+
+  <div ng-if="ctrl.step === 2">
+    <div class="gf-form-group" ng-if="ctrl.dash.gnetId">
+      <h3 class="section-heading">
+        Importing Dashboard from
+        <a href="https://grafana.com/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.com</a>
+      </h3>
+
+      <div class="gf-form">
+        <label class="gf-form-label width-15">Published by</label>
+        <label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
+      </div>
+      <div class="gf-form">
+        <label class="gf-form-label width-15">Updated on</label>
+        <label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
+      </div>
+    </div>
+
+    <h3 class="section-heading">
+      Options
+    </h3>
+
+    <div class="gf-form-group">
+      <div class="gf-form-inline">
+        <div class="gf-form gf-form--grow">
+          <label class="gf-form-label width-15">Name</label>
+          <input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
+          <label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.dash.title">
+            <i class="fa fa-check"></i>
+          </label>
+        </div>
+      </div>
+
+      <div class="gf-form-inline" ng-if="ctrl.nameExists">
+        <div class="gf-form offset-width-15 gf-form--grow">
+          <label class="gf-form-label text-warning gf-form-label--grow">
+            <i class="fa fa-warning"></i>
+            A Dashboard with the same name already exists
+          </label>
+        </div>
+      </div>
+
+      <div class="gf-form-inline" ng-if="!ctrl.dash.title">
+        <div class="gf-form offset-width-15 gf-form--grow">
+          <label class="gf-form-label text-warning gf-form-label--grow">
+            <i class="fa fa-warning"></i>
+            A Dashboard should have a name
+          </label>
+        </div>
+      </div>
+
+      <div ng-repeat="input in ctrl.inputs">
+        <div class="gf-form">
+          <label class="gf-form-label width-15">
+            {{input.label}}
+            <info-popover mode="right-normal">
+              {{input.info}}
+            </info-popover>
+          </label>
+          <!-- Data source input -->
+          <div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
+            <select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
+              <option value="" ng-hide="input.value">{{input.info}}</option>
+            </select>
+          </div>
+          <!-- Constant input -->
+          <input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
+          <label class="gf-form-label text-success" ng-show="input.value">
+            <i class="fa fa-check"></i>
+          </label>
+        </div>
+      </div>
+    </div>
+
+    <div class="gf-form-button-row">
+      <button type="button" class="btn gf-form-btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
+        <i class="fa fa-save"></i> Import
+      </button>
+      <button type="button" class="btn gf-form-btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
+        <i class="fa fa-save"></i> Import (Overwrite)
+      </button>
+      <a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>
+    </div>
+
+  </div>
+</div>

+ 25 - 28
public/app/features/dashboard/specs/dash_import_ctrl_specs.ts → public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts

@@ -1,25 +1,24 @@
-import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+import {DashboardImportCtrl} from '../dashboard_import_ctrl';
+import config from '../../../core/config';
 
 
-import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
-import config from 'app/core/config';
-
-describe('DashImportCtrl', function() {
+describe('DashboardImportCtrl', function() {
   var ctx: any = {};
   var ctx: any = {};
-  var backendSrv = {
-    search: sinon.stub().returns(Promise.resolve([])),
-    get: sinon.stub()
-  };
 
 
-  beforeEach(angularMocks.module('grafana.core'));
+  let navModelSrv;
+  let backendSrv;
 
 
-  beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
-    ctx.$q = $q;
-    ctx.scope = $rootScope.$new();
-    ctx.ctrl = $controller(DashImportCtrl, {
-      $scope: ctx.scope,
-      backendSrv: backendSrv,
-    });
-  }));
+  beforeEach(() => {
+    navModelSrv = {
+      getNav: () => {}
+    };
+
+    backendSrv = {
+      search: jest.fn().mockReturnValue(Promise.resolve([])),
+      get: jest.fn()
+    };
+
+    ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {});
+  });
 
 
   describe('when uploading json', function() {
   describe('when uploading json', function() {
     beforeEach(function() {
     beforeEach(function() {
@@ -37,13 +36,13 @@ describe('DashImportCtrl', function() {
     });
     });
 
 
     it('should build input model', function() {
     it('should build input model', function() {
-      expect(ctx.ctrl.inputs.length).to.eql(1);
-      expect(ctx.ctrl.inputs[0].name).to.eql('ds');
-      expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
+      expect(ctx.ctrl.inputs.length).toBe(1);
+      expect(ctx.ctrl.inputs[0].name).toBe('ds');
+      expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source');
     });
     });
 
 
     it('should set inputValid to false', function() {
     it('should set inputValid to false', function() {
-      expect(ctx.ctrl.inputsValid).to.eql(false);
+      expect(ctx.ctrl.inputsValid).toBe(false);
     });
     });
   });
   });
 
 
@@ -51,7 +50,7 @@ describe('DashImportCtrl', function() {
     beforeEach(function() {
     beforeEach(function() {
       ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
       ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
       // setup api mock
       // setup api mock
-      backendSrv.get = sinon.spy(() => {
+      backendSrv.get = jest.fn(() => {
         return Promise.resolve({
         return Promise.resolve({
           json: {}
           json: {}
         });
         });
@@ -60,7 +59,7 @@ describe('DashImportCtrl', function() {
     });
     });
 
 
     it('should call gnet api with correct dashboard id', function() {
     it('should call gnet api with correct dashboard id', function() {
-      expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
+      expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123');
     });
     });
   });
   });
 
 
@@ -68,7 +67,7 @@ describe('DashImportCtrl', function() {
     beforeEach(function() {
     beforeEach(function() {
       ctx.ctrl.gnetUrl = '2342';
       ctx.ctrl.gnetUrl = '2342';
       // setup api mock
       // setup api mock
-      backendSrv.get = sinon.spy(() => {
+      backendSrv.get = jest.fn(() => {
         return Promise.resolve({
         return Promise.resolve({
           json: {}
           json: {}
         });
         });
@@ -77,10 +76,8 @@ describe('DashImportCtrl', function() {
     });
     });
 
 
     it('should call gnet api with correct dashboard id', function() {
     it('should call gnet api with correct dashboard id', function() {
-      expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
+      expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342');
     });
     });
   });
   });
 
 
 });
 });
-
-