@@ -60,16 +61,11 @@ export class OrgSwitchCtrl {
setUsingOrg(org) {
return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
- const re = /orgId=\d+/gi;
- this.setWindowLocationHref(this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId));
+ this.setWindowLocation(config.appSubUrl + (config.appSubUrl.endsWith('/') ? '' : '/') + '?orgId=' + org.orgId);
});
}
- getWindowLocationHref() {
- return window.location.href;
- }
-
- setWindowLocationHref(href: string) {
+ setWindowLocation(href: string) {
window.location.href = href;
}
}
diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts
index 138da186238..5fd6e16c466 100644
--- a/public/app/core/components/query_part/query_part_editor.ts
+++ b/public/app/core/components/query_part/query_part_editor.ts
@@ -23,11 +23,13 @@ export function queryPartEditorDirective($compile, templateSrv) {
scope: {
part: '=',
handleEvent: '&',
+ debounce: '@',
},
link: function postLink($scope, elem) {
var part = $scope.part;
var partDef = part.def;
var $paramsContainer = elem.find('.query-part-parameters');
+ var debounceLookup = $scope.debounce;
$scope.partActions = [];
@@ -128,6 +130,10 @@ export function queryPartEditorDirective($compile, templateSrv) {
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
+
+ if (debounceLookup) {
+ typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
+ }
}
$scope.showActionsMenu = function() {
diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts
index 99245ed3331..720334d8973 100644
--- a/public/app/core/components/scroll/scroll.ts
+++ b/public/app/core/components/scroll/scroll.ts
@@ -6,7 +6,9 @@ export function geminiScrollbar() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
- let scrollbar = new PerfectScrollbar(elem[0]);
+ let scrollbar = new PerfectScrollbar(elem[0], {
+ wheelPropagation: true,
+ });
let lastPos = 0;
appEvents.on(
diff --git a/public/app/core/directives/dash_class.js b/public/app/core/directives/dash_class.js
index 9df53bdbd48..4a139272632 100644
--- a/public/app/core/directives/dash_class.js
+++ b/public/app/core/directives/dash_class.js
@@ -18,10 +18,6 @@ function (_, $, coreModule) {
elem.toggleClass('panel-in-fullscreen', false);
});
- $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) {
- elem.toggleClass('playlist-active', newValue === true);
- });
-
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
if (newValue) {
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js
index 2754f8d8c6e..7ba4a5a5259 100644
--- a/public/app/core/directives/metric_segment.js
+++ b/public/app/core/directives/metric_segment.js
@@ -22,6 +22,7 @@ function (_, $, coreModule) {
segment: "=",
getOptions: "&",
onChange: "&",
+ debounce: "@",
},
link: function($scope, elem) {
var $input = $(inputTemplate);
@@ -30,6 +31,7 @@ function (_, $, coreModule) {
var options = null;
var cancelBlur = null;
var linkMode = true;
+ var debounceLookup = $scope.debounce;
$input.appendTo(elem);
$button.appendTo(elem);
@@ -135,6 +137,10 @@ function (_, $, coreModule) {
return items ? this.process(items) : items;
};
+ if (debounceLookup) {
+ typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
+ }
+
$button.keydown(function(evt) {
// trigger typeahead on down arrow or enter key
if (evt.keyCode === 40 || evt.keyCode === 13) {
diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts
index 7bee5cdfec6..9f32e21f3f6 100644
--- a/public/app/core/services/search_srv.ts
+++ b/public/app/core/services/search_srv.ts
@@ -150,9 +150,9 @@ export class SearchSrv {
if (hit.folderId) {
section = {
id: hit.folderId,
- uid: hit.uid,
+ uid: hit.folderUid,
title: hit.folderTitle,
- url: hit.url,
+ url: hit.folderUrl,
items: [],
icon: 'fa fa-folder-open',
toggle: this.toggleFolder.bind(this),
diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.jest.ts
new file mode 100644
index 00000000000..bbb894094ff
--- /dev/null
+++ b/public/app/core/specs/file_export.jest.ts
@@ -0,0 +1,64 @@
+import * as fileExport from '../utils/file_export';
+import { beforeEach, expect } from 'test/lib/common';
+
+describe('file_export', () => {
+ let ctx: any = {};
+
+ beforeEach(() => {
+ ctx.seriesList = [
+ {
+ alias: 'series_1',
+ datapoints: [
+ [1, 1500026100000],
+ [2, 1500026200000],
+ [null, 1500026300000],
+ [null, 1500026400000],
+ [null, 1500026500000],
+ [6, 1500026600000],
+ ],
+ },
+ {
+ alias: 'series_2',
+ datapoints: [[11, 1500026100000], [12, 1500026200000], [13, 1500026300000], [15, 1500026500000]],
+ },
+ ];
+
+ ctx.timeFormat = 'X'; // Unix timestamp (seconds)
+ });
+
+ describe('when exporting series as rows', () => {
+ it('should export points in proper order', () => {
+ let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
+ const expectedText =
+ 'Series;Time;Value\n' +
+ 'series_1;1500026100;1\n' +
+ 'series_1;1500026200;2\n' +
+ 'series_1;1500026300;null\n' +
+ 'series_1;1500026400;null\n' +
+ 'series_1;1500026500;null\n' +
+ 'series_1;1500026600;6\n' +
+ 'series_2;1500026100;11\n' +
+ 'series_2;1500026200;12\n' +
+ 'series_2;1500026300;13\n' +
+ 'series_2;1500026500;15\n';
+
+ expect(text).toBe(expectedText);
+ });
+ });
+
+ describe('when exporting series as columns', () => {
+ it('should export points in proper order', () => {
+ let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
+ const expectedText =
+ 'Time;series_1;series_2\n' +
+ '1500026100;1;11\n' +
+ '1500026200;2;12\n' +
+ '1500026300;null;13\n' +
+ '1500026400;null;null\n' +
+ '1500026500;null;15\n' +
+ '1500026600;6;null\n';
+
+ expect(text).toBe(expectedText);
+ });
+ });
+});
diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts
index 8c640bc0630..4b51e6848d0 100644
--- a/public/app/core/specs/manage_dashboards.jest.ts
+++ b/public/app/core/specs/manage_dashboards.jest.ts
@@ -20,9 +20,6 @@ describe('ManageDashboards', () => {
icon: 'fa fa-folder',
tags: [],
isStarred: false,
- folderId: 410,
- folderTitle: 'afolder',
- folderSlug: 'afolder',
},
],
tags: [],
@@ -77,9 +74,6 @@ describe('ManageDashboards', () => {
icon: 'fa fa-folder',
tags: [],
isStarred: false,
- folderId: 410,
- folderTitle: 'afolder',
- folderSlug: 'afolder',
},
],
tags: [],
@@ -112,8 +106,9 @@ describe('ManageDashboards', () => {
tags: [],
isStarred: false,
folderId: 410,
- folderTitle: 'afolder',
- folderSlug: 'afolder',
+ folderUid: 'uid',
+ folderTitle: 'Folder',
+ folderUrl: '/dashboards/f/uid/folder',
},
{
id: 500,
diff --git a/public/app/core/specs/org_switcher.jest.ts b/public/app/core/specs/org_switcher.jest.ts
index 06172604069..230da81f191 100644
--- a/public/app/core/specs/org_switcher.jest.ts
+++ b/public/app/core/specs/org_switcher.jest.ts
@@ -7,6 +7,12 @@ jest.mock('app/core/services/context_srv', () => ({
},
}));
+jest.mock('app/core/config', () => {
+ return {
+ appSubUrl: '/subUrl',
+ };
+});
+
describe('OrgSwitcher', () => {
describe('when switching org', () => {
let expectedHref;
@@ -25,8 +31,7 @@ describe('OrgSwitcher', () => {
const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub);
- orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now';
- orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href);
+ orgSwitcherCtrl.setWindowLocation = href => (expectedHref = href);
return orgSwitcherCtrl.setUsingOrg({ orgId: 2 });
});
@@ -35,8 +40,8 @@ describe('OrgSwitcher', () => {
expect(expectedUsingUrl).toBe('/api/user/using/2');
});
- it('should switch orgId in url', () => {
- expect(expectedHref).toBe('http://localhost:3000?orgId=2&from=now-3h&to=now');
+ it('should switch orgId in url and redirect to home page', () => {
+ expect(expectedHref).toBe('/subUrl/?orgId=2');
});
});
});
diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts
index 444939d1f4a..550d11bbf9e 100644
--- a/public/app/core/specs/search_srv.jest.ts
+++ b/public/app/core/specs/search_srv.jest.ts
@@ -190,7 +190,9 @@ describe('SearchSrv', () => {
title: 'dash in folder1 1',
type: 'dash-db',
folderId: 1,
+ folderUid: 'uid',
folderTitle: 'folder1',
+ folderUrl: '/dashboards/f/uid/folder1',
},
])
);
@@ -206,6 +208,11 @@ describe('SearchSrv', () => {
it('should group results by folder', () => {
expect(results).toHaveLength(2);
+ expect(results[0].id).toEqual(0);
+ expect(results[1].id).toEqual(1);
+ expect(results[1].uid).toEqual('uid');
+ expect(results[1].title).toEqual('folder1');
+ expect(results[1].url).toEqual('/dashboards/f/uid/folder1');
});
});
diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts
index 1ebd5b90a86..670326fc068 100644
--- a/public/app/core/utils/file_export.ts
+++ b/public/app/core/utils/file_export.ts
@@ -3,19 +3,27 @@ import moment from 'moment';
import { saveAs } from 'file-saver';
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
+const POINT_TIME_INDEX = 1;
+const POINT_VALUE_INDEX = 0;
-export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
_.each(seriesList, function(series) {
_.each(series.datapoints, function(dp) {
- text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
+ text +=
+ series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n';
});
});
+ return text;
+}
+
+export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+ var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel);
saveSaveBlob(text, 'grafana_data_export.csv');
}
-export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
- var text = (excel ? 'sep=;\n' : '') + 'Time;';
+export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+ let text = (excel ? 'sep=;\n' : '') + 'Time;';
// add header
_.each(seriesList, function(series) {
text += series.alias + ';';
@@ -24,14 +32,15 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
text += '\n';
// process data
+ seriesList = mergeSeriesByTime(seriesList);
var dataArr = [[]];
var sIndex = 1;
_.each(seriesList, function(series) {
var cIndex = 0;
dataArr.push([]);
_.each(series.datapoints, function(dp) {
- dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat);
- dataArr[sIndex][cIndex] = dp[0];
+ dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat);
+ dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX];
cIndex++;
});
sIndex++;
@@ -46,6 +55,44 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
text = text.substring(0, text.length - 1);
text += '\n';
}
+
+ return text;
+}
+
+/**
+ * Collect all unique timestamps from series list and use it to fill
+ * missing points by null.
+ */
+function mergeSeriesByTime(seriesList) {
+ let timestamps = [];
+ for (let i = 0; i < seriesList.length; i++) {
+ let seriesPoints = seriesList[i].datapoints;
+ for (let j = 0; j < seriesPoints.length; j++) {
+ timestamps.push(seriesPoints[j][POINT_TIME_INDEX]);
+ }
+ }
+ timestamps = _.sortedUniq(timestamps.sort());
+
+ for (let i = 0; i < seriesList.length; i++) {
+ let seriesPoints = seriesList[i].datapoints;
+ let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]);
+ let extendedSeries = [];
+ let pointIndex;
+ for (let j = 0; j < timestamps.length; j++) {
+ pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]);
+ if (pointIndex !== -1) {
+ extendedSeries.push(seriesPoints[pointIndex]);
+ } else {
+ extendedSeries.push([null, timestamps[j]]);
+ }
+ }
+ seriesList[i].datapoints = extendedSeries;
+ }
+ return seriesList;
+}
+
+export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+ let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel);
saveSaveBlob(text, 'grafana_data_export.csv');
}
diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts
index 0d6a92d24e3..5c8bd276f76 100644
--- a/public/app/features/dashboard/create_folder_ctrl.ts
+++ b/public/app/features/dashboard/create_folder_ctrl.ts
@@ -1,4 +1,5 @@
import appEvents from 'app/core/app_events';
+import locationUtil from 'app/core/utils/location_util';
export class CreateFolderCtrl {
title = '';
@@ -19,7 +20,7 @@ export class CreateFolderCtrl {
return this.backendSrv.createFolder({ title: this.title }).then(result => {
appEvents.emit('alert-success', ['Folder Created', 'OK']);
- this.$location.url(result.url);
+ this.$location.url(locationUtil.stripBaseFromUrl(result.url));
});
}
@@ -27,7 +28,7 @@ export class CreateFolderCtrl {
this.titleTouched = true;
this.validationSrv
- .validateNewDashboardOrFolderName(this.title)
+ .validateNewFolderName(this.title)
.then(() => {
this.hasValidationError = false;
})
diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts
index 86fe12eab8a..d127e628a77 100644
--- a/public/app/features/dashboard/dashboard_import_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_import_ctrl.ts
@@ -18,7 +18,7 @@ export class DashboardImportCtrl {
nameValidationError: any;
/** @ngInject */
- constructor(private backendSrv, private validationSrv, navModelSrv, private $location, private $scope, $routeParams) {
+ constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
this.navModel = navModelSrv.getNav('create', 'import');
this.step = 1;
@@ -93,7 +93,7 @@ export class DashboardImportCtrl {
this.nameExists = false;
this.validationSrv
- .validateNewDashboardOrFolderName(this.dash.title)
+ .validateNewDashboardName(0, this.dash.title)
.then(() => {
this.hasNameValidationError = false;
})
@@ -124,8 +124,7 @@ export class DashboardImportCtrl {
inputs: inputs,
})
.then(res => {
- this.$location.url('dashboard/' + res.importedUri);
- this.$scope.dismiss();
+ this.$location.url(res.importedUrl);
});
}
diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts
index 08ab70053f6..a1ff99007e3 100644
--- a/public/app/features/dashboard/dashboard_srv.ts
+++ b/public/app/features/dashboard/dashboard_srv.ts
@@ -1,5 +1,6 @@
import coreModule from 'app/core/core_module';
import { DashboardModel } from './dashboard_model';
+import locationUtil from 'app/core/utils/location_util';
export class DashboardSrv {
dash: any;
@@ -19,7 +20,10 @@ export class DashboardSrv {
return this.dash;
}
- handleSaveDashboardError(clone, err) {
+ handleSaveDashboardError(clone, options, err) {
+ options = options || {};
+ options.overwrite = true;
+
if (err.data && err.data.status === 'version-mismatch') {
err.isHandled = true;
@@ -30,7 +34,7 @@ export class DashboardSrv {
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
- this.save(clone, { overwrite: true });
+ this.save(clone, options);
},
});
}
@@ -40,12 +44,12 @@ export class DashboardSrv {
this.$rootScope.appEvent('confirm-modal', {
title: 'Conflict',
- text: 'Dashboard with the same name exists.',
+ text: 'A dashboard with the same name in selected folder already exists.',
text2: 'Would you still like to save this dashboard?',
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
- this.save(clone, { overwrite: true });
+ this.save(clone, options);
},
});
}
@@ -74,7 +78,7 @@ export class DashboardSrv {
this.dash.version = data.version;
if (data.url !== this.$location.path()) {
- this.$location.url(data.url);
+ this.$location.url(locationUtil.stripBaseFromUrl(data.url)).replace();
}
this.$rootScope.appEvent('dashboard-saved', this.dash);
@@ -90,7 +94,7 @@ export class DashboardSrv {
return this.backendSrv
.saveDashboard(clone, options)
.then(this.postSave.bind(this, clone))
- .catch(this.handleSaveDashboardError.bind(this, clone));
+ .catch(this.handleSaveDashboardError.bind(this, clone, options));
}
saveDashboard(options, clone) {
diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
index 01d16436a45..aeb840c317a 100644
--- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
+++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
@@ -93,7 +93,6 @@ export class AddPanelPanel extends React.Component
this.onAddPanel(panel)} title={panel.name}>
diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts
index 5eb3d094641..56284a877c5 100644
--- a/public/app/features/dashboard/folder_picker/folder_picker.ts
+++ b/public/app/features/dashboard/folder_picker/folder_picker.ts
@@ -67,7 +67,7 @@ export class FolderPickerCtrl {
this.newFolderNameTouched = true;
this.validationSrv
- .validateNewDashboardOrFolderName(this.newFolderName)
+ .validateNewFolderName(this.newFolderName)
.then(() => {
this.hasValidationError = false;
})
diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/history/history.ts
index 2a3fd0b3984..d9f0c087438 100644
--- a/public/app/features/dashboard/history/history.ts
+++ b/public/app/features/dashboard/history/history.ts
@@ -4,6 +4,7 @@ import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
+import locationUtil from 'app/core/utils/location_util';
import { DashboardModel } from '../dashboard_model';
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
@@ -185,7 +186,7 @@ export class HistoryListCtrl {
return this.historySrv
.restoreDashboard(this.dashboard, version)
.then(response => {
- this.$location.path('dashboard/db/' + response.slug);
+ this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
this.$route.reload();
this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
})
diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html
index 47c3bae2ef6..5103fab8b75 100644
--- a/public/app/features/dashboard/settings/settings.html
+++ b/public/app/features/dashboard/settings/settings.html
@@ -96,13 +96,14 @@
diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts
index c538f171fa0..e9d5c6180be 100755
--- a/public/app/features/dashboard/settings/settings.ts
+++ b/public/app/features/dashboard/settings/settings.ts
@@ -14,6 +14,7 @@ export class SettingsCtrl {
canSave: boolean;
canDelete: boolean;
sections: any[];
+ hasUnsavedFolderChange: boolean;
/** @ngInject */
constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) {
@@ -38,6 +39,7 @@ export class SettingsCtrl {
this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 });
+ this.$rootScope.onAppEvent('dashboard-saved', this.onPostSave.bind(this), $scope);
}
buildSectionList() {
@@ -135,6 +137,10 @@ export class SettingsCtrl {
this.dashboardSrv.saveDashboard();
}
+ onPostSave() {
+ this.hasUnsavedFolderChange = false;
+ }
+
hideSettings() {
var urlParams = this.$location.search();
delete urlParams.editview;
@@ -195,7 +201,15 @@ export class SettingsCtrl {
onFolderChange(folder) {
this.dashboard.meta.folderId = folder.id;
this.dashboard.meta.folderTitle = folder.title;
- this.dashboard.meta.folderSlug = folder.slug;
+ this.hasUnsavedFolderChange = true;
+ }
+
+ getFolder() {
+ return {
+ id: this.dashboard.meta.folderId,
+ title: this.dashboard.meta.folderTitle,
+ url: this.dashboard.meta.folderUrl,
+ };
}
}
diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
index 739485b235d..1cb59ef5bac 100644
--- a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
+++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
@@ -19,10 +19,10 @@ describe('DashboardImportCtrl', function() {
};
validationSrv = {
- validateNewDashboardOrFolderName: jest.fn().mockReturnValue(Promise.resolve()),
+ validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
};
- ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}, {});
+ ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
});
describe('when uploading json', function() {
diff --git a/public/app/features/dashboard/validation_srv.ts b/public/app/features/dashboard/validation_srv.ts
index 8460f4efa71..817be7ca0e3 100644
--- a/public/app/features/dashboard/validation_srv.ts
+++ b/public/app/features/dashboard/validation_srv.ts
@@ -1,13 +1,27 @@
import coreModule from 'app/core/core_module';
+const hitTypes = {
+ FOLDER: 'dash-folder',
+ DASHBOARD: 'dash-db',
+};
+
export class ValidationSrv {
rootName = 'general';
/** @ngInject */
constructor(private $q, private backendSrv) {}
- validateNewDashboardOrFolderName(name) {
+ validateNewDashboardName(folderId, name) {
+ return this.validate(folderId, name, 'A dashboard in this folder with the same name already exists');
+ }
+
+ validateNewFolderName(name) {
+ return this.validate(0, name, 'A folder or dashboard in the general folder with the same name already exists');
+ }
+
+ private validate(folderId, name, existingErrorMessage) {
name = (name || '').trim();
+ const nameLowerCased = name.toLowerCase();
if (name.length === 0) {
return this.$q.reject({
@@ -16,7 +30,7 @@ export class ValidationSrv {
});
}
- if (name.toLowerCase() === this.rootName) {
+ if (folderId === 0 && nameLowerCased === this.rootName) {
return this.$q.reject({
type: 'EXISTING',
message: 'This is a reserved name and cannot be used for a folder.',
@@ -25,12 +39,26 @@ export class ValidationSrv {
let deferred = this.$q.defer();
- this.backendSrv.search({ query: name }).then(res => {
- for (let hit of res) {
- if (name.toLowerCase() === hit.title.toLowerCase()) {
+ const promises = [];
+ promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name }));
+ promises.push(this.backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name }));
+
+ this.$q.all(promises).then(res => {
+ let hits = [];
+
+ if (res.length > 0 && res[0].length > 0) {
+ hits = res[0];
+ }
+
+ if (res.length > 1 && res[1].length > 0) {
+ hits = hits.concat(res[1]);
+ }
+
+ for (let hit of hits) {
+ if (nameLowerCased === hit.title.toLowerCase()) {
deferred.reject({
type: 'EXISTING',
- message: 'A folder or dashboard with the same name already exists',
+ message: existingErrorMessage,
});
break;
}
diff --git a/public/app/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html
index 9f06ebdb017..3fce8b3c720 100644
--- a/public/app/features/org/partials/team_details.html
+++ b/public/app/features/org/partials/team_details.html
@@ -33,7 +33,7 @@
Old picker
-->
-
+
diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index 01730e2fede..dec7868a553 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
// update scrollbar after mounting
ctrl.events.on('component-did-mount', () => {
if (ctrl.__proto__.constructor.scrollable) {
- panelScrollbar = new PerfectScrollbar(panelContent[0]);
+ panelScrollbar = new PerfectScrollbar(panelContent[0], {
+ wheelPropagation: true,
+ });
}
});
diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts
index 323a88ddaee..2c7698db08e 100644
--- a/public/app/features/panel/solo_panel_ctrl.ts
+++ b/public/app/features/panel/solo_panel_ctrl.ts
@@ -9,7 +9,7 @@ export class SoloPanelCtrl {
$scope.init = function() {
contextSrv.sidemenu = false;
- appEvents.emit('toggle-sidemenu');
+ appEvents.emit('toggle-sidemenu-hidden');
var params = $location.search();
panelId = parseInt(params.panelId);
diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html
index ff655f0c33a..fec7ba190ec 100644
--- a/public/app/features/plugins/import_list/import_list.html
+++ b/public/app/features/plugins/import_list/import_list.html
@@ -6,7 +6,7 @@