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/folder_picker/folder_picker.html b/public/app/features/dashboard/folder_picker/folder_picker.html
new file mode 100644
index 00000000000..b830e65b7e6
--- /dev/null
+++ b/public/app/features/dashboard/folder_picker/folder_picker.html
@@ -0,0 +1,46 @@
+
+
\ No newline at end of file
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..72d1984634c
--- /dev/null
+++ b/public/app/features/dashboard/folder_picker/folder_picker.ts
@@ -0,0 +1,161 @@
+///
+
+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/validation_srv.ts b/public/app/features/dashboard/validation_srv.ts
new file mode 100644
index 00000000000..4e02b8a678f
--- /dev/null
+++ b/public/app/features/dashboard/validation_srv.ts
@@ -0,0 +1,48 @@
+///
+
+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 {