Commit cc2349f6 by Torkel Ödegaard

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

parents 31bb2f80 4d7ff4de
...@@ -90,12 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { ...@@ -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"},
}, },
}) })
} }
......
...@@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { ...@@ -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',
......
...@@ -15,7 +15,6 @@ import './unsavedChangesSrv'; ...@@ -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'; ...@@ -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);
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 = '';
}
}
<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>
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'; describe('DashboardImportCtrl', function() {
import config from 'app/core/config';
describe('DashImportCtrl', 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) => { beforeEach(() => {
ctx.$q = $q; navModelSrv = {
ctx.scope = $rootScope.$new(); getNav: () => {}
ctx.ctrl = $controller(DashImportCtrl, { };
$scope: ctx.scope,
backendSrv: backendSrv, 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() { ...@@ -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.length).toBe(1);
expect(ctx.ctrl.inputs[0].name).to.eql('ds'); expect(ctx.ctrl.inputs[0].name).toBe('ds');
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); 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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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');
}); });
}); });
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment