Browse Source

Merge pull request #10278 from grafana/10197_new_folder

Create new folder from the folder picker component
Marcus Efraimsson 8 years ago
parent
commit
9184300398

+ 1 - 1
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"
     })

+ 1 - 1
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: []
     };

+ 29 - 28
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";

+ 15 - 17
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;
+      });
   }
 }

+ 18 - 15
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() {

+ 46 - 0
public/app/features/dashboard/folder_picker/folder_picker.html

@@ -0,0 +1,46 @@
+<div class="gf-form-inline">
+  <div class="gf-form">
+    <label class="gf-form-label {{ctrl.labelClass}}">Folder</label>
+    <div class="dropdown" ng-hide="ctrl.createNewFolder">
+      <gf-form-dropdown model="ctrl.folder"
+        get-options="ctrl.getOptions($query)"
+        on-change="ctrl.onFolderChange($option)">
+      </gf-form-dropdown>
+    </div>
+    <input type="text"
+      class="gf-form-input max-width-10"
+      ng-show="ctrl.createNewFolder"
+      give-focus="ctrl.createNewFolder"
+      ng-model="ctrl.newFolderName"
+      ng-model-options="{ debounce: 400 }"
+      ng-class="{'validation-error': !ctrl.isNewFolderNameValid()}"
+      ng-change="ctrl.newFolderNameChanged()" />
+  </div>
+  <div class="gf-form" ng-show="ctrl.createNewFolder">
+    <label class="gf-form-label text-success"
+      ng-show="ctrl.newFolderNameTouched && !ctrl.hasValidationError">
+      <i class="fa fa-check"></i>
+    </label>
+  </div>
+  <div class="gf-form" ng-show="ctrl.createNewFolder">
+    <button class="gf-form-label"
+      ng-click="ctrl.createFolder($event)"
+      ng-disabled="!ctrl.newFolderNameTouched || ctrl.hasValidationError">
+      <i class="fa fa-fw fa-save"></i>&nbsp;Create
+    </button>
+  </div>
+  <div class="gf-form" ng-show="ctrl.createNewFolder">
+    <button class="gf-form-label"
+      ng-click="ctrl.cancelCreateFolder($event)">
+      Cancel
+  </button>
+  </div>
+</div>
+<div class="gf-form-inline" ng-if="ctrl.newFolderNameTouched && ctrl.hasValidationError">
+  <div class="gf-form gf-form--grow">
+    <label class="gf-form-label text-warning gf-form-label--grow">
+      <i class="fa fa-warning"></i>
+      {{ctrl.validationError}}
+    </label>
+  </div>
+</div>

+ 170 - 0
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);

+ 0 - 103
public/app/features/dashboard/folder_picker/picker.ts

@@ -1,103 +0,0 @@
-///<reference path="../../../headers/common.d.ts" />
-
-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 = `
-<div class="gf-form">
-  <label class="gf-form-label {{ctrl.labelClass}}">Folder</label>
-  <div class="dropdown">
-    <gf-form-dropdown model="ctrl.folder"
-      get-options="ctrl.getOptions($query)"
-      on-change="ctrl.onFolderChange($option)">
-    </gf-form-dropdown>
-  </div>
-</div>
-`;
-
-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);

+ 4 - 1
public/app/features/dashboard/move_to_folder_modal/move_to_folder.html

@@ -18,12 +18,15 @@
           <folder-picker
             on-load="ctrl.onFolderChange($folder)"
             on-change="ctrl.onFolderChange($folder)"
+            enter-folder-creation="ctrl.onEnterFolderCreation()"
+            exit-folder-creation="ctrl.onExitFolderCreation()"
+            enable-create-new="true"
             label-class="width-7">
           </folder-picker>
       </div>
     </div>
     <div class="gf-form-button-row text-center">
-      <button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Move</button>
+      <button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid || !ctrl.isValidFolderSelection">Move</button>
       <a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
     </div>
   </form>

+ 9 - 0
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() {

+ 6 - 15
public/app/features/dashboard/partials/create_folder.html

@@ -7,34 +7,25 @@
   <form name="ctrl.saveForm" ng-submit="ctrl.create()" novalidate>
 		<div class="gf-form-inline">
 			<div class="gf-form gf-form--grow">
-				<label class="gf-form-label width-10">Folder name</label>
-				<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-model-options="{ debounce: 400 }" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
-				<label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.title">
+				<label class="gf-form-label width-10">Name</label>
+				<input type="text" class="gf-form-input" ng-model="ctrl.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-model-options="{ debounce: 400 }" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
+				<label class="gf-form-label text-success" ng-if="ctrl.titleTouched && !ctrl.hasValidationError">
 					<i class="fa fa-check"></i>
 				</label>
 			</div>
 		</div>
 
-		<div class="gf-form-inline" ng-if="ctrl.nameExists">
+		<div class="gf-form-inline" ng-if="ctrl.hasValidationError">
 			<div class="gf-form offset-width-10 gf-form--grow">
 				<label class="gf-form-label text-warning gf-form-label--grow">
 					<i class="fa fa-warning"></i>
-					A Folder or Dashboard with the same name already exists
-				</label>
-			</div>
-		</div>
-
-		<div class="gf-form-inline" ng-if="!ctrl.title && ctrl.titleTouched">
-			<div class="gf-form offset-width-10 gf-form--grow">
-				<label class="gf-form-label text-warning gf-form-label--grow">
-					<i class="fa fa-warning"></i>
-					A Folder should have a name
+					{{ctrl.validationError}}
 				</label>
 			</div>
 		</div>
 
 		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-success width-12" ng-disabled="ctrl.nameExists || ctrl.title.length === 0">
+			<button type="submit" class="btn btn-success width-12" ng-disabled="!ctrl.titleTouched || ctrl.hasValidationError">
 				<i class="fa fa-save"></i> Create
 			</button>
 		</div>

+ 3 - 12
public/app/features/dashboard/partials/dashboardImport.html → public/app/features/dashboard/partials/dashboard_import.html

@@ -65,26 +65,17 @@
         <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">
+          <label class="gf-form-label text-success" ng-if="ctrl.titleTouched && !ctrl.hasNameValidationError">
             <i class="fa fa-check"></i>
           </label>
         </div>
       </div>
 
-      <div class="gf-form-inline" ng-if="ctrl.nameExists">
+      <div class="gf-form-inline" ng-if="ctrl.hasNameValidationError">
         <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
+            {{ctrl.nameValidationError}}
           </label>
         </div>
       </div>

+ 14 - 2
public/app/features/dashboard/save_as_modal.ts

@@ -22,13 +22,16 @@ const template = `
       <div class="gf-form">
         <folder-picker initial-folder-id="ctrl.folderId"
                        on-change="ctrl.onFolderChange($folder)"
+                       enter-folder-creation="ctrl.onEnterFolderCreation()"
+                       exit-folder-creation="ctrl.onExitFolderCreation()"
+                       enable-create-new="true"
                        label-class="width-7">
         </folder-picker>
       </div>
 		</div>
 
 		<div class="gf-form-button-row text-center">
-			<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Save</button>
+			<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid || !ctrl.isValidFolderSelection">Save</button>
 			<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
 		</div>
 	</form>
@@ -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();
     }
   }

+ 5 - 3
public/app/features/dashboard/settings/settings.html

@@ -45,9 +45,11 @@
 			</bootstrap-tagsinput>
 		</div>
 		<folder-picker initial-title="ctrl.dashboard.meta.folderTitle"
-								 initial-folder-id="ctrl.dashboard.meta.folderId"
-				 on-change="ctrl.onFolderChange($folder)"
-		 label-class="width-7">
+									 initial-folder-id="ctrl.dashboard.meta.folderId"
+									 on-change="ctrl.onFolderChange($folder)"
+									 enable-create-new="true"
+									 is-valid-selection="true"
+									 label-class="width-7">
 		</folder-picker>
 		<gf-form-switch class="gf-form" label="Editable" tooltip="Uncheck, then save and reload to disable all dashboard editing" checked="ctrl.dashboard.editable" label-class="width-7">
 		</gf-form-switch>

+ 6 - 1
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() {

+ 46 - 0
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);

+ 4 - 0
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 {