diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts
index f2e4fa1ec88..666ca811037 100644
--- a/public/app/core/routes/routes.ts
+++ b/public/app/core/routes/routes.ts
@@ -65,7 +65,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
})
.when("/dashboard/import", {
templateUrl:
- "public/app/features/dashboard/partials/dashboardImport.html",
+ "public/app/features/dashboard/partials/dashboard_import.html",
controller: "DashboardImportCtrl",
controllerAs: "ctrl"
})
diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts
index 5b276a53e4f..bc34d9d37ab 100644
--- a/public/app/core/services/backend_srv.ts
+++ b/public/app/core/services/backend_srv.ts
@@ -251,7 +251,7 @@ export class BackendSrv {
createDashboardFolder(name) {
const dash = {
schemaVersion: 16,
- title: name,
+ title: name.trim(),
editable: true,
panels: []
};
diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts
index 7bf0a980205..14517bea7bb 100644
--- a/public/app/features/dashboard/all.ts
+++ b/public/app/features/dashboard/all.ts
@@ -1,31 +1,32 @@
-import "./dashboard_ctrl";
-import "./alerting_srv";
-import "./history/history";
-import "./dashboardLoaderSrv";
-import "./dashnav/dashnav";
-import "./submenu/submenu";
-import "./save_as_modal";
-import "./save_modal";
-import "./shareModalCtrl";
-import "./shareSnapshotCtrl";
-import "./dashboard_srv";
-import "./view_state_srv";
-import "./time_srv";
-import "./unsavedChangesSrv";
-import "./unsaved_changes_modal";
-import "./timepicker/timepicker";
-import "./upload";
-import "./export/export_modal";
-import "./export_data/export_data_modal";
-import "./ad_hoc_filters";
-import "./repeat_option/repeat_option";
-import "./dashgrid/DashboardGridDirective";
-import "./dashgrid/PanelLoader";
-import "./dashgrid/RowOptions";
-import "./acl/acl";
-import "./folder_picker/picker";
-import "./move_to_folder_modal/move_to_folder";
-import "./settings/settings";
+import './dashboard_ctrl';
+import './alerting_srv';
+import './history/history';
+import './dashboardLoaderSrv';
+import './dashnav/dashnav';
+import './submenu/submenu';
+import './save_as_modal';
+import './save_modal';
+import './shareModalCtrl';
+import './shareSnapshotCtrl';
+import './dashboard_srv';
+import './view_state_srv';
+import './validation_srv';
+import './time_srv';
+import './unsavedChangesSrv';
+import './unsaved_changes_modal';
+import './timepicker/timepicker';
+import './upload';
+import './export/export_modal';
+import './export_data/export_data_modal';
+import './ad_hoc_filters';
+import './repeat_option/repeat_option';
+import './dashgrid/DashboardGridDirective';
+import './dashgrid/PanelLoader';
+import './dashgrid/RowOptions';
+import './acl/acl';
+import './folder_picker/folder_picker';
+import './move_to_folder_modal/move_to_folder';
+import './settings/settings';
import coreModule from "app/core/core_module";
import { DashboardListCtrl } from "./dashboard_list_ctrl";
diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts
index c4a8429f45e..d9eaf95d693 100644
--- a/public/app/features/dashboard/create_folder_ctrl.ts
+++ b/public/app/features/dashboard/create_folder_ctrl.ts
@@ -3,23 +3,22 @@ import appEvents from "app/core/app_events";
export class CreateFolderCtrl {
title = "";
navModel: any;
- nameExists = false;
titleTouched = false;
+ hasValidationError: boolean;
+ validationError: any;
/** @ngInject **/
- constructor(private backendSrv, private $location, navModelSrv) {
- this.navModel = navModelSrv.getNav("dashboards", "manage-dashboards", 0);
+ constructor(private backendSrv, private $location, private validationSrv, navModelSrv) {
+ this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
}
create() {
- if (!this.title || this.title.trim().length === 0) {
+ if (this.hasValidationError) {
return;
}
- const title = this.title.trim();
-
- return this.backendSrv.createDashboardFolder(title).then(result => {
- appEvents.emit("alert-success", ["Folder Created", "OK"]);
+ return this.backendSrv.createDashboardFolder(this.title).then(result => {
+ appEvents.emit('alert-success', ['Folder Created', 'OK']);
var folderUrl = `/dashboards/folder/${result.dashboard.id}/${
result.meta.slug
@@ -31,14 +30,13 @@ export class CreateFolderCtrl {
titleChanged() {
this.titleTouched = true;
- this.backendSrv.search({ query: this.title }).then(res => {
- this.nameExists = false;
- for (let hit of res) {
- if (this.title === hit.title) {
- this.nameExists = true;
- break;
- }
- }
- });
+ this.validationSrv.validateNewDashboardOrFolderName(this.title)
+ .then(() => {
+ this.hasValidationError = false;
+ })
+ .catch(err => {
+ this.hasValidationError = true;
+ this.validationError = err.message;
+ });
}
}
diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts
index 51b2d696dac..2dc33fe07f7 100644
--- a/public/app/features/dashboard/dashboard_import_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_import_ctrl.ts
@@ -13,16 +13,13 @@ export class DashboardImportCtrl {
gnetUrl: string;
gnetError: string;
gnetInfo: any;
+ titleTouched: boolean;
+ hasNameValidationError: boolean;
+ nameValidationError: any;
/** @ngInject */
- constructor(
- private backendSrv,
- navModelSrv,
- private $location,
- private $scope,
- $routeParams
- ) {
- this.navModel = navModelSrv.getNav("create", "import");
+ constructor(private backendSrv, private validationSrv, navModelSrv, private $location, private $scope, $routeParams) {
+ this.navModel = navModelSrv.getNav('create', 'import');
this.step = 1;
this.nameExists = false;
@@ -93,15 +90,21 @@ export class DashboardImportCtrl {
}
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.titleTouched = true;
+ this.nameExists = false;
+
+ this.validationSrv.validateNewDashboardOrFolderName(this.dash.title)
+ .then(() => {
+ this.hasNameValidationError = false;
+ })
+ .catch(err => {
+ if (err.type === 'EXISTING') {
this.nameExists = true;
- break;
}
- }
- });
+
+ this.hasNameValidationError = true;
+ this.nameValidationError = err.message;
+ });
}
saveDashboard() {
diff --git a/public/app/features/dashboard/folder_picker/folder_picker.html b/public/app/features/dashboard/folder_picker/folder_picker.html
new file mode 100644
index 00000000000..8722a19a5f8
--- /dev/null
+++ b/public/app/features/dashboard/folder_picker/folder_picker.html
@@ -0,0 +1,46 @@
+
+
diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts
new file mode 100644
index 00000000000..150f29a0dac
--- /dev/null
+++ b/public/app/features/dashboard/folder_picker/folder_picker.ts
@@ -0,0 +1,170 @@
+import _ from "lodash";
+import coreModule from "app/core/core_module";
+import appEvents from "app/core/app_events";
+
+export class FolderPickerCtrl {
+ initialTitle: string;
+ initialFolderId?: number;
+ labelClass: string;
+ onChange: any;
+ onLoad: any;
+ onCreateFolder: any;
+ enterFolderCreation: any;
+ exitFolderCreation: any;
+ enableCreateNew: boolean;
+ rootName = "Root";
+ folder: any;
+ createNewFolder: boolean;
+ newFolderName: string;
+ newFolderNameTouched: boolean;
+ hasValidationError: boolean;
+ validationError: any;
+
+ /** @ngInject */
+ constructor(private backendSrv, private validationSrv) {
+ if (!this.labelClass) {
+ this.labelClass = "width-7";
+ }
+
+ this.loadInitialValue();
+ }
+
+ getOptions(query) {
+ var params = {
+ query: query,
+ type: "dash-folder"
+ };
+
+ return this.backendSrv.search(params).then(result => {
+ if (
+ query === "" ||
+ query.toLowerCase() === "r" ||
+ query.toLowerCase() === "ro" ||
+ query.toLowerCase() === "roo" ||
+ query.toLowerCase() === "root"
+ ) {
+ result.unshift({ title: this.rootName, id: 0 });
+ }
+
+ if (this.enableCreateNew && query === "") {
+ result.unshift({ title: "-- New Folder --", id: -1 });
+ }
+
+ return _.map(result, item => {
+ return { text: item.title, value: item.id };
+ });
+ });
+ }
+
+ onFolderChange(option) {
+ if (option.value === -1) {
+ this.createNewFolder = true;
+ this.enterFolderCreation();
+ return;
+ }
+ this.onChange({ $folder: { id: option.value, title: option.text } });
+ }
+
+ newFolderNameChanged() {
+ this.newFolderNameTouched = true;
+
+ this.validationSrv
+ .validateNewDashboardOrFolderName(this.newFolderName)
+ .then(() => {
+ this.hasValidationError = false;
+ })
+ .catch(err => {
+ this.hasValidationError = true;
+ this.validationError = err.message;
+ });
+ }
+
+ createFolder(evt) {
+ if (evt) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ }
+
+ return this.backendSrv
+ .createDashboardFolder(this.newFolderName)
+ .then(result => {
+ appEvents.emit("alert-success", ["Folder Created", "OK"]);
+
+ this.closeCreateFolder();
+ this.folder = {
+ text: result.dashboard.title,
+ value: result.dashboard.id
+ };
+ this.onFolderChange(this.folder);
+ });
+ }
+
+ cancelCreateFolder(evt) {
+ if (evt) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ }
+
+ this.closeCreateFolder();
+ this.loadInitialValue();
+ }
+
+ private closeCreateFolder() {
+ this.exitFolderCreation();
+ this.createNewFolder = false;
+ this.hasValidationError = false;
+ this.validationError = null;
+ this.newFolderName = "";
+ this.newFolderNameTouched = false;
+ }
+
+ private loadInitialValue() {
+ if (this.initialFolderId && this.initialFolderId > 0) {
+ this.getOptions("").then(result => {
+ this.folder = _.find(result, { value: this.initialFolderId });
+ this.onFolderLoad();
+ });
+ } else {
+ if (this.initialTitle) {
+ this.folder = { text: this.initialTitle, value: null };
+ } else {
+ this.folder = { text: this.rootName, value: 0 };
+ }
+
+ this.onFolderLoad();
+ }
+ }
+
+ private onFolderLoad() {
+ if (this.onLoad) {
+ this.onLoad({
+ $folder: { id: this.folder.value, title: this.folder.text }
+ });
+ }
+ }
+}
+
+export function folderPicker() {
+ return {
+ restrict: "E",
+ templateUrl:
+ "public/app/features/dashboard/folder_picker/folder_picker.html",
+ controller: FolderPickerCtrl,
+ bindToController: true,
+ controllerAs: "ctrl",
+ scope: {
+ initialTitle: "<",
+ initialFolderId: "<",
+ labelClass: "@",
+ rootName: "@",
+ onChange: "&",
+ onLoad: "&",
+ onCreateFolder: "&",
+ enterFolderCreation: "&",
+ exitFolderCreation: "&",
+ enableCreateNew: "@"
+ }
+ };
+}
+
+coreModule.directive("folderPicker", folderPicker);
diff --git a/public/app/features/dashboard/folder_picker/picker.ts b/public/app/features/dashboard/folder_picker/picker.ts
deleted file mode 100644
index f04065b36f5..00000000000
--- a/public/app/features/dashboard/folder_picker/picker.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-///
-
-import coreModule from "app/core/core_module";
-import _ from "lodash";
-
-export class FolderPickerCtrl {
- initialTitle: string;
- initialFolderId?: number;
- labelClass: string;
- onChange: any;
- onLoad: any;
- rootName = "Root";
- folder: any;
-
- /** @ngInject */
- constructor(private backendSrv) {
- if (!this.labelClass) {
- this.labelClass = "width-7";
- }
-
- if (this.initialFolderId && this.initialFolderId > 0) {
- this.getOptions("").then(result => {
- this.folder = _.find(result, { value: this.initialFolderId });
- this.onFolderLoad();
- });
- } else {
- if (this.initialTitle) {
- this.folder = { text: this.initialTitle, value: null };
- } else {
- this.folder = { text: this.rootName, value: 0 };
- }
-
- this.onFolderLoad();
- }
- }
-
- getOptions(query) {
- var params = {
- query: query,
- type: "dash-folder"
- };
-
- return this.backendSrv.search(params).then(result => {
- if (
- query === "" ||
- query.toLowerCase() === "r" ||
- query.toLowerCase() === "ro" ||
- query.toLowerCase() === "roo" ||
- query.toLowerCase() === "root"
- ) {
- result.unshift({ title: this.rootName, id: 0 });
- }
-
- return _.map(result, item => {
- return { text: item.title, value: item.id };
- });
- });
- }
-
- onFolderLoad() {
- if (this.onLoad) {
- this.onLoad({
- $folder: { id: this.folder.value, title: this.folder.text }
- });
- }
- }
-
- onFolderChange(option) {
- this.onChange({ $folder: { id: option.value, title: option.text } });
- }
-}
-
-const template = `
-
-`;
-
-export function folderPicker() {
- return {
- restrict: "E",
- template: template,
- controller: FolderPickerCtrl,
- bindToController: true,
- controllerAs: "ctrl",
- scope: {
- initialTitle: "<",
- initialFolderId: "<",
- labelClass: "@",
- rootName: "@",
- onChange: "&",
- onLoad: "&"
- }
- };
-}
-
-coreModule.directive("folderPicker", folderPicker);
diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html
index 8f04d393a5d..8a67517aa92 100644
--- a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html
+++ b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html
@@ -18,12 +18,15 @@
diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts
index 9f252783cf7..84fc16833a1 100644
--- a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts
+++ b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts
@@ -6,6 +6,7 @@ export class MoveToFolderCtrl {
folder: any;
dismiss: any;
afterSave: any;
+ isValidFolderSelection = true;
/** @ngInject */
constructor(private backendSrv) {}
@@ -39,6 +40,14 @@ export class MoveToFolderCtrl {
return this.afterSave();
});
}
+
+ onEnterFolderCreation() {
+ this.isValidFolderSelection = false;
+ }
+
+ onExitFolderCreation() {
+ this.isValidFolderSelection = true;
+ }
}
export function moveToFolderModal() {
diff --git a/public/app/features/dashboard/partials/create_folder.html b/public/app/features/dashboard/partials/create_folder.html
index 22dbaab1419..21b6dfc1661 100644
--- a/public/app/features/dashboard/partials/create_folder.html
+++ b/public/app/features/dashboard/partials/create_folder.html
@@ -7,34 +7,25 @@
@@ -38,6 +41,7 @@ const template = `
export class SaveDashboardAsModalCtrl {
clone: any;
folderId: any;
+ isValidFolderSelection = true;
dismiss: () => void;
/** @ngInject */
@@ -68,8 +72,16 @@ export class SaveDashboardAsModalCtrl {
return this.dashboardSrv.save(this.clone).then(this.dismiss);
}
+ onEnterFolderCreation() {
+ this.isValidFolderSelection = false;
+ }
+
+ onExitFolderCreation() {
+ this.isValidFolderSelection = true;
+ }
+
keyDown(evt) {
- if (evt.keyCode === 13) {
+ if (this.isValidFolderSelection && evt.keyCode === 13) {
this.save();
}
}
diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html
index 03d8b8592a9..0411850c543 100644
--- a/public/app/features/dashboard/settings/settings.html
+++ b/public/app/features/dashboard/settings/settings.html
@@ -45,9 +45,11 @@
+ initial-folder-id="ctrl.dashboard.meta.folderId"
+ on-change="ctrl.onFolderChange($folder)"
+ enable-create-new="true"
+ is-valid-selection="true"
+ label-class="width-7">
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 a8590f5cbd3..0abef382de8 100644
--- a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
+++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts
@@ -6,6 +6,7 @@ describe("DashboardImportCtrl", function() {
let navModelSrv;
let backendSrv;
+ let validationSrv;
beforeEach(() => {
navModelSrv = {
@@ -17,7 +18,11 @@ describe("DashboardImportCtrl", function() {
get: jest.fn()
};
- ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {});
+ validationSrv = {
+ validateNewDashboardOrFolderName: jest.fn().mockReturnValue(Promise.resolve())
+ };
+
+ 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
new file mode 100644
index 00000000000..4200cf1b6ba
--- /dev/null
+++ b/public/app/features/dashboard/validation_srv.ts
@@ -0,0 +1,46 @@
+import coreModule from "app/core/core_module";
+
+export class ValidationSrv {
+ rootName = "root";
+
+ /** @ngInject */
+ constructor(private $q, private backendSrv) {}
+
+ validateNewDashboardOrFolderName(name) {
+ name = (name || "").trim();
+
+ if (name.length === 0) {
+ return this.$q.reject({
+ type: "REQUIRED",
+ message: "Name is required"
+ });
+ }
+
+ if (name.toLowerCase() === this.rootName) {
+ return this.$q.reject({
+ type: "EXISTING",
+ message: "A folder or dashboard with the same name already exists"
+ });
+ }
+
+ let deferred = this.$q.defer();
+
+ this.backendSrv.search({ query: name }).then(res => {
+ for (let hit of res) {
+ if (name.toLowerCase() === hit.title.toLowerCase()) {
+ deferred.reject({
+ type: "EXISTING",
+ message: "A folder or dashboard with the same name already exists"
+ });
+ break;
+ }
+ }
+
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+ }
+}
+
+coreModule.service("validationSrv", ValidationSrv);
diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss
index 3a61246c494..10a900f82f9 100644
--- a/public/sass/components/_gf-form.scss
+++ b/public/sass/components/_gf-form.scss
@@ -109,6 +109,10 @@ $input-border: 1px solid $input-border-color;
&--error {
color: $critical;
}
+
+ &:disabled {
+ color: $text-color-weak
+ }
}
.gf-form-label + .gf-form-label {