Browse Source

dashfolders: rough draft of bulk edit

Daniel Lee 8 years ago
parent
commit
7f3293ce80

+ 6 - 0
pkg/api/index.go

@@ -217,6 +217,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 						{Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"},
 					},
 				},
+				{
+					Text:        "Dashboard List",
+					Description: "Manage Dashboards And Folders",
+					Id:          "dashboards",
+					Url:         setting.AppSubUrl + "/dashboards",
+				},
 				{
 					Text:        "Preferences",
 					Id:          "org",

+ 5 - 4
public/app/core/routes/routes.ts

@@ -48,10 +48,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     reloadOnSearch: false,
     pageClass: 'page-dashboard',
   })
-  .when('/dashboards/list', {
-    templateUrl: 'public/app/features/dashboard/partials/dash_list.html',
-    controller : 'DashListCtrl',
-  })
   .when('/configuration', {
     templateUrl: 'public/app/features/admin/partials/configuration_home.html',
     controller : 'ConfigurationHomeCtrl',
@@ -73,6 +69,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     controller : 'DataSourceEditCtrl',
     controllerAs: 'ctrl',
   })
+  .when('/dashboards', {
+    templateUrl: 'public/app/features/dashboard/partials/dashboardList.html',
+    controller : 'DashboardListCtrl',
+    controllerAs: 'ctrl',
+  })
   .when('/org', {
     templateUrl: 'public/app/features/org/partials/orgDetails.html',
     controller : 'OrgDetailsCtrl',

+ 0 - 2
public/app/features/admin/admin_list_users_ctrl.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 export default class AdminListUsersCtrl {
   users: any;
   pages = [];

+ 0 - 32
public/app/features/dashboard/all.js

@@ -1,32 +0,0 @@
-define([
-  './dashboard_ctrl',
-  './alerting_srv',
-  './history/history',
-  './dashboardLoaderSrv',
-  './dashnav/dashnav',
-  './submenu/submenu',
-  './save_as_modal',
-  './save_modal',
-  './shareModalCtrl',
-  './shareSnapshotCtrl',
-  './dashboard_srv',
-  './viewStateSrv',
-  './time_srv',
-  './unsavedChangesSrv',
-  './unsaved_changes_modal',
-  './timepicker/timepicker',
-  './impression_store',
-  './upload',
-  './import/dash_import',
-  './export/export_modal',
-  './export_data/export_data_modal',
-  './ad_hoc_filters',
-  './repeat_option/repeat_option',
-  './dashgrid/DashboardGrid',
-  './dashgrid/PanelLoader',
-  './dashgrid/RowOptions',
-  './acl/acl',
-  './acl/acl',
-  './folder_picker/picker',
-  './folder_modal/folder'
-], function () {});

+ 36 - 0
public/app/features/dashboard/all.ts

@@ -0,0 +1,36 @@
+
+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 './viewStateSrv';
+import './time_srv';
+import './unsavedChangesSrv';
+import './unsaved_changes_modal';
+import './timepicker/timepicker';
+import './impression_store';
+import './upload';
+import './import/dash_import';
+import './export/export_modal';
+import './export_data/export_data_modal';
+import './ad_hoc_filters';
+import './repeat_option/repeat_option';
+import './dashgrid/DashboardGrid';
+import './dashgrid/PanelLoader';
+import './dashgrid/RowOptions';
+import './acl/acl';
+import './folder_picker/picker';
+import './folder_modal/folder';
+import './move_to_folder_modal/move_to_folder';
+import coreModule from 'app/core/core_module';
+
+import {DashboardListCtrl} from './dashboard_list_ctrl';
+
+coreModule.controller('DashboardListCtrl', DashboardListCtrl);

+ 117 - 0
public/app/features/dashboard/dashboard_list_ctrl.ts

@@ -0,0 +1,117 @@
+import _ from 'lodash';
+import appEvents from 'app/core/app_events';
+
+export class DashboardListCtrl {
+  public dashboards: any [];
+  query: any;
+  navModel: any;
+  canDelete = false;
+  canMove = false;
+
+  /** @ngInject */
+  constructor(private backendSrv, navModelSrv, private $q) {
+    this.navModel = navModelSrv.getNav('cfg', 'dashboards');
+    this.query = '';
+    this.getDashboards();
+  }
+
+  getDashboards() {
+    return this.backendSrv.get(`/api/search?query=${this.query}&mode=tree`).then((result) => {
+
+      this.dashboards = this.groupDashboardsInFolders(result);
+
+      for (let dash of this.dashboards) {
+        dash.checked = false;
+      }
+    });
+  }
+
+  groupDashboardsInFolders(results) {
+    let byId = _.groupBy(results, 'id');
+    let byFolderId = _.groupBy(results, 'folderId');
+    let finalList = [];
+
+    // add missing parent folders
+    _.each(results, (hit, index) => {
+      if (hit.folderId && !byId[hit.folderId]) {
+        const folder = {
+          id: hit.folderId,
+          uri: `db/${hit.folderSlug}`,
+          title: hit.folderTitle,
+          type: 'dash-folder'
+        };
+        byId[hit.folderId] = folder;
+        results.splice(index, 0, folder);
+      }
+    });
+
+    // group by folder
+    for (let hit of results) {
+      if (hit.folderId) {
+        hit.type = "dash-child";
+      } else {
+        finalList.push(hit);
+      }
+
+      hit.url = 'dashboard/' + hit.uri;
+
+      if (hit.type === 'dash-folder') {
+        if (!byFolderId[hit.id]) {
+          continue;
+        }
+
+        for (let child of byFolderId[hit.id]) {
+          finalList.push(child);
+        }
+      }
+    }
+
+    return finalList;
+  }
+
+  selectionChanged() {
+    const selected = _.filter(this.dashboards, {checked: true}).length;
+    this.canDelete = selected > 0;
+
+    const selectedDashboards = _.filter(this.dashboards, (o) => {
+      return o.checked && (o.type === 'dash-db' || o.type === 'dash-child');
+    }).length;
+
+    const selectedFolders = _.filter(this.dashboards, {checked: true, type: 'dash-folder'}).length;
+    this.canMove = selectedDashboards > 0 && selectedFolders === 0;
+  }
+
+  delete() {
+    const selectedDashboards =  _.filter(this.dashboards, {checked: true});
+
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`,
+      icon: 'fa-trash',
+      yesText: 'Delete',
+      onConfirm: () => {
+        const promises = [];
+        for (let dash of selectedDashboards) {
+          promises.push(this.backendSrv.delete(`/api/dashboards/${dash.uri}`));
+        }
+
+        this.$q.all(promises).then(() => {
+          this.getDashboards();
+        });
+      }
+    });
+  }
+
+  moveTo() {
+    const selectedDashboards =  _.filter(this.dashboards, {checked: true});
+
+    const template = '<move-to-folder-modal dismiss="dismiss()" ' +
+      'dashboards="model.dashboards" after-save="model.afterSave()">' +
+      '</move-to-folder-modal>`';
+    appEvents.emit('show-modal', {
+      templateHtml: template,
+      modalClass: 'modal--narrow',
+      model: {dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this)}
+    });
+  }
+}

+ 29 - 0
public/app/features/dashboard/move_to_folder_modal/move_to_folder.html

@@ -0,0 +1,29 @@
+<div class="modal-body">
+  <div class="modal-header">
+    <h2 class="modal-header-title">
+			<i class="gicon gicon-folder-new"></i>
+      <span class="p-l-1">Choose Dashboard Folder</span>
+    </h2>
+
+    <a class="modal-header-close" ng-click="ctrl.dismiss();">
+      <i class="fa fa-remove"></i>
+    </a>
+  </div>
+
+  <form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content folder-modal" novalidate>
+    <p>Move the {{ctrl.dashboards.length}} selected dashboards to the following folder:</p>
+
+    <div class="p-t-2">
+      <div class="gf-form">
+          <folder-picker initial-title="Choose"
+            on-change="ctrl.onFolderChange($folder)"
+            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>
+      <a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
+    </div>
+  </form>
+</div>

+ 61 - 0
public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts

@@ -0,0 +1,61 @@
+import coreModule from 'app/core/core_module';
+import appEvents from 'app/core/app_events';
+import {DashboardModel} from '../dashboard_model';
+
+export class MoveToFolderCtrl {
+  dashboards: any;
+  folder: any;
+  dismiss: any;
+  afterSave: any;
+
+  /** @ngInject */
+  constructor(private backendSrv, private $q) {}
+
+  onFolderChange(folder) {
+    this.folder = folder;
+  }
+
+  save() {
+    const promises = [];
+    for (let dash of this.dashboards) {
+      const promise = this.backendSrv.get('/api/dashboards/' + dash.uri).then(fullDash => {
+        const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
+        model.folderId = this.folder.id;
+        model.meta.folderId = this.folder.id;
+        model.meta.folderTitle = this.folder.title;
+        const clone = model.getSaveModelClone();
+        return this.backendSrv.saveDashboard(clone);
+      });
+
+      promises.push(promise);
+    }
+
+    return this.$q.all(promises).then(() => {
+      appEvents.emit('alert-success', ['Dashboards Moved', 'OK']);
+      this.dismiss();
+
+      return this.afterSave();
+    }).then(() => {
+      console.log('afterSave');
+    }).catch(err => {
+      appEvents.emit('alert-error', [err.message]);
+    });
+  }
+}
+
+export function moveToFolderModal() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/dashboard/move_to_folder_modal/move_to_folder.html',
+    controller: MoveToFolderCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      dismiss: "&",
+      dashboards: "=",
+      afterSave: "&"
+    }
+  };
+}
+
+coreModule.directive('moveToFolderModal', moveToFolderModal);

+ 88 - 0
public/app/features/dashboard/partials/dashboardList.html

@@ -0,0 +1,88 @@
+<div class="scroll-canvas">
+  <div gemini-scrollbar>
+    <navbar model="ctrl.navModel"></navbar>
+    <div class="page-container" style="height: 95%">
+      <div class="page-header">
+        <h1>Dashboards</h1>
+
+        <a class="btn btn-success" href="/dashboard/new">
+          <i class="fa fa-plus"></i>
+          Create Dashboard
+        </a>
+        <a class="btn btn-success" href="/dashboard/new/?editview=new-folder">
+          <i class="fa fa-plus"></i>
+          Create Folder
+        </a>
+      </div>
+      <div class="gf-form width-15 gf-form-group">
+        <span style="position: relative;">
+          <input type="text" class="gf-form-input" placeholder="Find Dashboard by name" tabindex="1" give-focus="true"
+            ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.getDashboards()" />
+        </span>
+      </div>
+
+      <div class="gf-form-group" ng-if="ctrl.dashboards.length > 1">
+        <div class="gf-form-button-row">
+          <button	type="button"
+              class="btn gf-form-button btn-secondary"
+              ng-disabled="!ctrl.canMove"
+              ng-click="ctrl.moveTo()"
+              bs-tooltip="ctrl.canMove ? '' : 'Select a dashboard to move (cannot move folders)'" data-placement="bottom">
+            <i class="fa fa-exchange"></i>&nbsp;&nbsp;Move to...
+          </button>
+          <button  type="button"
+              class="btn gf-form-button btn-inverse"
+              ng-click="ctrl.delete()"
+              ng-disabled="!ctrl.canDelete">
+              <i class="fa fa-trash"></i>&nbsp;&nbsp;Delete
+          </button>
+        </div>
+      </div>
+
+        <div class="admin-list-table" style="height: 80%">
+          <div gemini-scrollbar>
+            <table class="filter-table form-inline" ng-show="ctrl.dashboards.length > 0">
+              <thead>
+                <tr>
+                  <th class="width-4"></th>
+                  <th></th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr bindonce ng-repeat="dashboard in ctrl.dashboards">
+                  <td class="filter-table__switch-cell" bs-tooltip="" data-placement="right">
+                    <gf-form-switch
+                        switch-class="gf-form-switch--table-cell"
+                        on-change="ctrl.selectionChanged()"
+                        checked="dashboard.checked">
+                    </gf-form-switch>
+                  </td>
+                  <td>
+                    <a class="search-item pointer search-item--{{dashboard.type}}"
+                      bo-href-i="{{dashboard.url}}">
+                      <span class="search-result-tags">
+                        <span ng-click="ctrl.filterByTag(tag, $event)" bindonce ng-repeat="tag in dashboard.tags" tag-color-from-name="tag"  class="label label-tag">
+                          {{tag}}
+                        </span>
+                        <i class="fa" bo-class="{'fa-star': dashboard.isStarred, 'fa-star-o': !dashboard.isStarred}"></i>
+                      </span>
+                      <span class="search-result-link">
+                        <i class="fa search-result-icon"></i>
+                        <span bo-text="dashboard.title"></span>
+                      </span>
+                    </a>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+
+      <em class="muted" ng-hide="ctrl.dashboards.length > 0">
+        No Dashboards or Folders found.
+      </em>
+    </div>
+
+  </div>
+</div>
+

+ 174 - 0
public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts

@@ -0,0 +1,174 @@
+import {DashboardListCtrl} from '../dashboard_list_ctrl';
+import q from 'q';
+
+describe('DashboardListCtrl', () => {
+  describe('when fetching dashboards', () => {
+    let ctrl;
+
+    describe('and dashboard has parent that is not in search result', () => {
+      beforeEach(() => {
+        const response = [
+          {
+            id: 399,
+            title: "Dashboard Test",
+            uri: "db/dashboard-test",
+            type: "dash-db",
+            tags: [],
+            isStarred: false,
+            folderId: 410,
+            folderTitle: "afolder",
+            folderSlug: "afolder"
+          }
+        ];
+
+        ctrl = new DashboardListCtrl({get: () => q.resolve(response)}, {getNav: () => {}}, q);
+        return ctrl.getDashboards();
+      });
+
+      it('should add the missing parent folder to the result', () => {
+        expect(ctrl.dashboards.length).toEqual(2);
+        expect(ctrl.dashboards[0].id).toEqual(410);
+        expect(ctrl.dashboards[1].id).toEqual(399);
+      });
+    });
+
+    beforeEach(() => {
+      const response = [
+        {
+          id: 410,
+          title: "afolder",
+          uri: "db/afolder",
+          type: "dash-folder",
+          tags: [],
+          isStarred: false
+        },
+        {
+          id: 3,
+          title: "something else",
+          uri: "db/something-else",
+          type: "dash-db",
+          tags: [],
+          isStarred: false,
+        },
+        {
+          id: 399,
+          title: "Dashboard Test",
+          uri: "db/dashboard-test",
+          type: "dash-db",
+          tags: [],
+          isStarred: false,
+          folderId: 410,
+          folderTitle: "afolder",
+          folderSlug: "afolder"
+        }
+      ];
+      ctrl = new DashboardListCtrl({get: () => q.resolve(response)}, {getNav: () => {}}, null);
+      return ctrl.getDashboards();
+    });
+
+    it('should group them in folders', () => {
+      expect(ctrl.dashboards.length).toEqual(3);
+      expect(ctrl.dashboards[0].id).toEqual(410);
+      expect(ctrl.dashboards[1].id).toEqual(399);
+      expect(ctrl.dashboards[2].id).toEqual(3);
+    });
+  });
+
+  describe('when selecting dashboards', () => {
+    let ctrl;
+
+    beforeEach(() => {
+      ctrl = new DashboardListCtrl({get: () => q.resolve([])}, {getNav: () => {}}, null);
+    });
+
+    describe('and no dashboards are selected', () => {
+      beforeEach(() => {
+        ctrl.dashboards = [
+          {id: 1, type: 'dash-folder'},
+          {id: 2, type: 'dash-db'}
+        ];
+        ctrl.selectionChanged();
+      });
+
+      it('should disable Move To button', () => {
+        expect(ctrl.canMove).toBeFalsy();
+      });
+
+      it('should disable delete button', () => {
+        expect(ctrl.canDelete).toBeFalsy();
+      });
+    });
+
+    describe('and one dashboard in root is selected', () => {
+      beforeEach(() => {
+        ctrl.dashboards = [
+          {id: 1, type: 'dash-folder'},
+          {id: 2, type: 'dash-db', checked: true}
+        ];
+        ctrl.selectionChanged();
+      });
+
+      it('should enable Move To button', () => {
+        expect(ctrl.canMove).toBeTruthy();
+      });
+
+      it('should enable delete button', () => {
+        expect(ctrl.canDelete).toBeTruthy();
+      });
+    });
+
+    describe('and one child dashboard is selected', () => {
+      beforeEach(() => {
+        ctrl.dashboards = [
+          {id: 1, type: 'dash-folder'},
+          {id: 2, type: 'dash-child', checked: true}
+        ];
+        ctrl.selectionChanged();
+      });
+
+      it('should enable Move To button', () => {
+        expect(ctrl.canMove).toBeTruthy();
+      });
+
+      it('should enable delete button', () => {
+        expect(ctrl.canDelete).toBeTruthy();
+      });
+    });
+
+    describe('and one child dashboard and one dashboard is selected', () => {
+      beforeEach(() => {
+        ctrl.dashboards = [
+          {id: 1, type: 'dash-folder'},
+          {id: 2, type: 'dash-child', checked: true}
+        ];
+        ctrl.selectionChanged();
+      });
+
+      it('should enable Move To button', () => {
+        expect(ctrl.canMove).toBeTruthy();
+      });
+
+      it('should enable delete button', () => {
+        expect(ctrl.canDelete).toBeTruthy();
+      });
+    });
+
+    describe('and one child dashboard and one folder is selected', () => {
+      beforeEach(() => {
+        ctrl.dashboards = [
+          {id: 1, type: 'dash-folder', checked: true},
+          {id: 2, type: 'dash-child', checked: true}
+        ];
+        ctrl.selectionChanged();
+      });
+
+      it('should disable Move To button', () => {
+        expect(ctrl.canMove).toBeFalsy();
+      });
+
+      it('should enable delete button', () => {
+        expect(ctrl.canDelete).toBeTruthy();
+      });
+    });
+  });
+});

+ 0 - 0
public/sass/components/_dash_list.scss