瀏覽代碼

dashfolders: convert folder settings to React

Daniel Lee 8 年之前
父節點
當前提交
545d7b9477

+ 2 - 2
pkg/api/index.go

@@ -102,8 +102,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 	}
 
 	dashboardChildNavs := []*dtos.NavLink{
-		{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
-		{Divider: true, HideFromTabs: true},
+		{Text: "Home", Id: "home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
+		{Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true},
 		{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
 		{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
 		{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},

+ 2 - 0
public/app/containers/IContainerProps.ts

@@ -3,6 +3,7 @@ import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore'
 import { NavStore } from './../stores/NavStore/NavStore';
 import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
 import { ViewStore } from './../stores/ViewStore/ViewStore';
+import { FolderStore } from './../stores/FolderStore/FolderStore';
 
 interface IContainerProps {
   search: typeof SearchStore.Type;
@@ -10,6 +11,7 @@ interface IContainerProps {
   nav: typeof NavStore.Type;
   alertList: typeof AlertListStore.Type;
   view: typeof ViewStore.Type;
+  folder: typeof FolderStore.Type;
 }
 
 export default IContainerProps;

+ 78 - 0
public/app/containers/ManageDashboards/FolderSettings.jest.tsx

@@ -0,0 +1,78 @@
+import React from 'react';
+import { FolderSettings } from './FolderSettings';
+import { RootStore } from 'app/stores/RootStore/RootStore';
+import { backendSrv } from 'test/mocks/common';
+import { shallow } from 'enzyme';
+
+describe('FolderSettings', () => {
+  let wrapper;
+  let page;
+
+  beforeAll(() => {
+    backendSrv.getDashboard.mockReturnValue(
+      Promise.resolve({
+        dashboard: {
+          id: 1,
+          title: 'Folder Name',
+        },
+        meta: {
+          slug: 'folder-name',
+          canSave: true,
+        },
+      })
+    );
+
+    const store = RootStore.create(
+      {},
+      {
+        backendSrv: backendSrv,
+      }
+    );
+
+    wrapper = shallow(<FolderSettings {...store} />);
+    return wrapper
+      .dive()
+      .instance()
+      .loadStore()
+      .then(() => {
+        page = wrapper.dive();
+      });
+  });
+
+  it('should set the title input field', () => {
+    const titleInput = page.find('.gf-form-input');
+    expect(titleInput).toHaveLength(1);
+    expect(titleInput.prop('value')).toBe('Folder Name');
+  });
+
+  it('should update title and enable save button when changed', () => {
+    const titleInput = page.find('.gf-form-input');
+    const disabledSubmitButton = page.find('button[type="submit"]');
+    expect(disabledSubmitButton.prop('disabled')).toBe(true);
+
+    titleInput.simulate('change', { target: { value: 'New Title' } });
+
+    const updatedTitleInput = page.find('.gf-form-input');
+    expect(updatedTitleInput.prop('value')).toBe('New Title');
+    const enabledSubmitButton = page.find('button[type="submit"]');
+    expect(enabledSubmitButton.prop('disabled')).toBe(false);
+  });
+
+  it('should disable save button if title is changed back to old title', () => {
+    const titleInput = page.find('.gf-form-input');
+
+    titleInput.simulate('change', { target: { value: 'Folder Name' } });
+
+    const enabledSubmitButton = page.find('button[type="submit"]');
+    expect(enabledSubmitButton.prop('disabled')).toBe(true);
+  });
+
+  it('should disable save button if title is changed to empty string', () => {
+    const titleInput = page.find('.gf-form-input');
+
+    titleInput.simulate('change', { target: { value: '' } });
+
+    const enabledSubmitButton = page.find('button[type="submit"]');
+    expect(enabledSubmitButton.prop('disabled')).toBe(true);
+  });
+});

+ 153 - 0
public/app/containers/ManageDashboards/FolderSettings.tsx

@@ -0,0 +1,153 @@
+import React from 'react';
+import { inject, observer } from 'mobx-react';
+import { toJS } from 'mobx';
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import IContainerProps from 'app/containers/IContainerProps';
+import { getSnapshot } from 'mobx-state-tree';
+import appEvents from 'app/core/app_events';
+
+@inject('nav', 'folder', 'view')
+@observer
+export class FolderSettings extends React.Component<IContainerProps, any> {
+  formSnapshot: any;
+  dashboard: any;
+
+  constructor(props) {
+    super(props);
+    this.loadStore();
+  }
+
+  loadStore() {
+    const { nav, folder, view } = this.props;
+
+    return folder.load(view.routeParams.get('slug') as string).then(res => {
+      this.formSnapshot = getSnapshot(folder);
+      this.dashboard = res.dashboard;
+
+      return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
+    });
+  }
+
+  onTitleChange(evt) {
+    this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value);
+  }
+
+  getFormSnapshot() {
+    if (!this.formSnapshot) {
+      this.formSnapshot = getSnapshot(this.props.folder);
+    }
+
+    return this.formSnapshot;
+  }
+
+  save(evt) {
+    if (evt) {
+      evt.stopPropagation();
+      evt.preventDefault();
+    }
+
+    const { nav, folder, view } = this.props;
+
+    folder
+      .saveDashboard(this.dashboard, { overwrite: false })
+      .then(newUrl => {
+        view.updatePathAndQuery(newUrl, '', '');
+
+        appEvents.emit('dashboard-saved');
+        appEvents.emit('alert-success', ['Folder saved']);
+      })
+      .then(() => {
+        return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
+      })
+      .catch(this.handleSaveFolderError);
+  }
+
+  delete(evt) {
+    if (evt) {
+      evt.stopPropagation();
+      evt.preventDefault();
+    }
+
+    const { folder, view } = this.props;
+    const title = folder.folder.title;
+
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: `Do you want to delete this folder and all its dashboards?`,
+      icon: 'fa-trash',
+      yesText: 'Delete',
+      onConfirm: () => {
+        return this.props.folder.deleteFolder().then(() => {
+          appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]);
+          view.updatePathAndQuery('dashboards', '', '');
+        });
+      },
+    });
+  }
+
+  handleSaveFolderError(err) {
+    if (err.data && err.data.status === 'version-mismatch') {
+      err.isHandled = true;
+
+      appEvents.emit('confirm-modal', {
+        title: 'Conflict',
+        text: 'Someone else has updated this folder.',
+        text2: 'Would you still like to save this folder?',
+        yesText: 'Save & Overwrite',
+        icon: 'fa-warning',
+        onConfirm: () => {
+          this.props.folder.saveDashboard(this.dashboard, { overwrite: true });
+        },
+      });
+    }
+
+    if (err.data && err.data.status === 'name-exists') {
+      err.isHandled = true;
+
+      appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
+    }
+  }
+
+  render() {
+    const { nav, folder } = this.props;
+
+    if (!folder.folder || !nav.main) {
+      return <h2>Loading</h2>;
+    }
+
+    return (
+      <div>
+        <PageHeader model={nav as any} />
+        <div className="page-container page-body">
+          <h2 className="page-sub-heading">Folder Settings</h2>
+
+          <div className="section gf-form-group">
+            <form name="folderSettingsForm" onSubmit={this.save.bind(this)}>
+              <div className="gf-form">
+                <label className="gf-form-label width-7">Name</label>
+                <input
+                  type="text"
+                  className="gf-form-input width-30"
+                  value={folder.folder.title}
+                  onChange={this.onTitleChange.bind(this)}
+                />
+              </div>
+              <div className="gf-form-button-row">
+                <button
+                  type="submit"
+                  className="btn btn-success"
+                  disabled={!folder.folder.canSave || !folder.folder.hasChanged}
+                >
+                  <i className="fa fa-trash" /> Save
+                </button>
+                <button className="btn btn-danger" onClick={this.delete.bind(this)} disabled={!folder.folder.canSave}>
+                  <i className="fa fa-trash" /> Delete
+                </button>
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}

+ 3 - 2
public/app/core/components/manage_dashboards/manage_dashboards.html

@@ -95,11 +95,12 @@
       </div>
     </div>
     <div class="search-results-container">
-        <dashboard-search-results
+      <dashboard-search-results
         results="ctrl.sections"
         editable="true"
         on-selection-changed="ctrl.selectionChanged()"
-        on-tag-selected="ctrl.filterByTag($tag)" />
+        on-tag-selected="ctrl.filterByTag($tag)"
+      />
     </div>
   </div>
 </div>

+ 3 - 3
public/app/core/services/bridge_srv.ts

@@ -10,7 +10,7 @@ export class BridgeSrv {
   private fullPageReloadRoutes;
 
   /** @ngInject */
-  constructor(private $location, private $timeout, private $window, private $rootScope) {
+  constructor(private $location, private $timeout, private $window, private $rootScope, private $route) {
     this.appSubUrl = config.appSubUrl;
     this.fullPageReloadRoutes = ['/logout'];
   }
@@ -29,14 +29,14 @@ export class BridgeSrv {
     this.$rootScope.$on('$routeUpdate', (evt, data) => {
       let angularUrl = this.$location.url();
       if (store.view.currentUrl !== angularUrl) {
-        store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
+        store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
       }
     });
 
     this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
       let angularUrl = this.$location.url();
       if (store.view.currentUrl !== angularUrl) {
-        store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
+        store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params);
       }
     });
 

+ 1 - 1
public/app/core/specs/bridge_srv.jest.ts

@@ -10,7 +10,7 @@ describe('BridgeSrv', () => {
   let searchSrv;
 
   beforeEach(() => {
-    searchSrv = new BridgeSrv(null, null, null, null);
+    searchSrv = new BridgeSrv(null, null, null, null, null);
   });
 
   describe('With /subUrl as appSubUrl', () => {

+ 14 - 14
public/app/features/dashboard/folder_page_loader.ts

@@ -43,33 +43,33 @@ export class FolderPageLoader {
       ctrl.navModel.main.text = '';
       ctrl.navModel.main.breadcrumbs = [{ title: 'Dashboards', url: 'dashboards' }, { title: folderTitle }];
 
-      const folderUrl = this.createFolderUrl(folderId, result.meta.type, result.meta.slug);
+      const folderUrl = this.createFolderUrl(folderId, result.meta.slug);
 
       const dashTab = _.find(ctrl.navModel.main.children, {
         id: 'manage-folder-dashboards',
       });
       dashTab.url = folderUrl;
 
-        if (result.meta.canAdmin) {
-          const permTab = _.find(ctrl.navModel.main.children, {
-            id: 'manage-folder-permissions',
-          });
+      if (result.meta.canAdmin) {
+        const permTab = _.find(ctrl.navModel.main.children, {
+          id: 'manage-folder-permissions',
+        });
 
-          permTab.url = folderUrl + '/permissions';
+        permTab.url = folderUrl + '/permissions';
 
-          const settingsTab = _.find(ctrl.navModel.main.children, {
-            id: 'manage-folder-settings',
-          });
-          settingsTab.url = folderUrl + '/settings';
-        } else {
-          ctrl.navModel.main.children = [dashTab];
-        }
+        const settingsTab = _.find(ctrl.navModel.main.children, {
+          id: 'manage-folder-settings',
+        });
+        settingsTab.url = folderUrl + '/settings';
+      } else {
+        ctrl.navModel.main.children = [dashTab];
+      }
 
       return result;
     });
   }
 
-  createFolderUrl(folderId: number, type: string, slug: string) {
+  createFolderUrl(folderId: number, slug: string) {
     return `dashboards/folder/${folderId}/${slug}`;
   }
 }

+ 1 - 1
public/app/features/dashboard/folder_settings_ctrl.ts

@@ -38,7 +38,7 @@ export class FolderSettingsCtrl {
     return this.backendSrv
       .saveDashboard(this.dashboard, { overwrite: false })
       .then(result => {
-        var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, this.meta.type, result.slug);
+        var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, result.slug);
         if (folderUrl !== this.$location.path()) {
           this.$location.url(folderUrl + '/settings');
         }

+ 5 - 3
public/app/routes/routes.ts

@@ -2,6 +2,7 @@ import './dashboard_loaders';
 import './ReactContainer';
 import { ServerStats } from 'app/containers/ServerStats/ServerStats';
 import { AlertRuleList } from 'app/containers/AlertRuleList/AlertRuleList';
+import { FolderSettings } from 'app/containers/ManageDashboards/FolderSettings';
 
 /** @ngInject **/
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -68,9 +69,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controllerAs: 'ctrl',
     })
     .when('/dashboards/folder/:folderId/:slug/settings', {
-      templateUrl: 'public/app/features/dashboard/partials/folder_settings.html',
-      controller: 'FolderSettingsCtrl',
-      controllerAs: 'ctrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => FolderSettings,
+      },
     })
     .when('/dashboards/folder/:folderId/:slug', {
       templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html',

+ 45 - 0
public/app/stores/FolderStore/FolderStore.ts

@@ -0,0 +1,45 @@
+import { types, getEnv, flow } from 'mobx-state-tree';
+
+export const Folder = types.model('Folder', {
+  id: types.identifier(types.number),
+  slug: types.string,
+  title: types.string,
+  canSave: types.boolean,
+  hasChanged: types.boolean,
+});
+
+export const FolderStore = types
+  .model('FolderStore', {
+    folder: types.maybe(Folder),
+  })
+  .actions(self => ({
+    load: flow(function* load(slug: string) {
+      const backendSrv = getEnv(self).backendSrv;
+      const res = yield backendSrv.getDashboard('db', slug);
+      self.folder = Folder.create({
+        id: res.dashboard.id,
+        title: res.dashboard.title,
+        slug: res.meta.slug,
+        canSave: res.meta.canSave,
+        hasChanged: false,
+      });
+      return res;
+    }),
+    setTitle: function(originalTitle: string, title: string) {
+      self.folder.title = title;
+      self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0;
+    },
+    saveDashboard: flow(function* saveDashboard(dashboard: any, options: any) {
+      const backendSrv = getEnv(self).backendSrv;
+      dashboard.title = self.folder.title.trim();
+
+      const res = yield backendSrv.saveDashboard(dashboard, options);
+      self.folder.slug = res.slug;
+      return `dashboards/folder/${self.folder.id}/${res.slug}/settings`;
+    }),
+    deleteFolder: flow(function* deleteFolder() {
+      const backendSrv = getEnv(self).backendSrv;
+
+      return backendSrv.deleteDashboard(self.folder.slug);
+    }),
+  }));

+ 47 - 0
public/app/stores/NavStore/NavStore.jest.ts

@@ -0,0 +1,47 @@
+import { NavStore } from './NavStore';
+
+describe('NavStore', () => {
+  const folderId = 1;
+  const folderTitle = 'Folder Name';
+  const folderSlug = 'folder-name';
+  const canAdmin = true;
+
+  const folder = {
+    id: folderId,
+    slug: folderSlug,
+    title: folderTitle,
+    canAdmin: canAdmin,
+  };
+
+  let store;
+
+  beforeEach(() => {
+    store = NavStore.create();
+    store.initFolderNav(folder, 'manage-folder-settings');
+  });
+
+  it('Should set text', () => {
+    expect(store.main.text).toBe(folderTitle);
+  });
+
+  it('Should load nav with tabs', () => {
+    expect(store.main.children.length).toBe(3);
+    expect(store.main.children[0].id).toBe('manage-folder-dashboards');
+    expect(store.main.children[1].id).toBe('manage-folder-permissions');
+    expect(store.main.children[2].id).toBe('manage-folder-settings');
+  });
+
+  it('Should set correct urls for each tab', () => {
+    expect(store.main.children.length).toBe(3);
+    expect(store.main.children[0].url).toBe(`dashboards/folder/${folderId}/${folderSlug}`);
+    expect(store.main.children[1].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/permissions`);
+    expect(store.main.children[2].url).toBe(`dashboards/folder/${folderId}/${folderSlug}/settings`);
+  });
+
+  it('Should set active tab', () => {
+    expect(store.main.children.length).toBe(3);
+    expect(store.main.children[0].active).toBe(false);
+    expect(store.main.children[1].active).toBe(false);
+    expect(store.main.children[2].active).toBe(true);
+  });
+});

+ 39 - 0
public/app/stores/NavStore/NavStore.ts

@@ -38,4 +38,43 @@ export const NavStore = types
       self.main = NavItem.create(main);
       self.node = NavItem.create(node);
     },
+    initFolderNav(folder: any, activeChildId: string) {
+      const folderUrl = createFolderUrl(folder.id, folder.slug);
+
+      self.main = {
+        icon: 'fa fa-folder-open',
+        id: 'manage-folder',
+        subTitle: 'Manage folder dashboards & permissions',
+        url: '',
+        text: folder.title,
+        breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }],
+        children: [
+          {
+            active: activeChildId === 'manage-folder-dashboards',
+            icon: 'fa fa-fw fa-th-large',
+            id: 'manage-folder-dashboards',
+            text: 'Dashboards',
+            url: folderUrl,
+          },
+          {
+            active: activeChildId === 'manage-folder-permissions',
+            icon: 'fa fa-fw fa-lock',
+            id: 'manage-folder-permissions',
+            text: 'Permissions',
+            url: folderUrl + '/permissions',
+          },
+          {
+            active: activeChildId === 'manage-folder-settings',
+            icon: 'fa fa-fw fa-cog',
+            id: 'manage-folder-settings',
+            text: 'Settings',
+            url: folderUrl + '/settings',
+          },
+        ],
+      };
+    },
   }));
+
+function createFolderUrl(folderId: number, slug: string) {
+  return `dashboards/folder/${folderId}/${slug}`;
+}

+ 3 - 0
public/app/stores/RootStore/RootStore.ts

@@ -4,6 +4,7 @@ import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore';
 import { NavStore } from './../NavStore/NavStore';
 import { AlertListStore } from './../AlertListStore/AlertListStore';
 import { ViewStore } from './../ViewStore/ViewStore';
+import { FolderStore } from './../FolderStore/FolderStore';
 
 export const RootStore = types.model({
   search: types.optional(SearchStore, {
@@ -19,7 +20,9 @@ export const RootStore = types.model({
   view: types.optional(ViewStore, {
     path: '',
     query: {},
+    routeParams: {},
   }),
+  folder: types.optional(FolderStore, {}),
 });
 
 type IRootStoreType = typeof RootStore.Type;

+ 10 - 1
public/app/stores/ViewStore/ViewStore.ts

@@ -15,6 +15,7 @@ export const ViewStore = types
   .model({
     path: types.string,
     query: types.map(QueryValueType),
+    routeParams: types.map(QueryValueType),
   })
   .views(self => ({
     get currentUrl() {
@@ -34,9 +35,17 @@ export const ViewStore = types
       }
     }
 
-    function updatePathAndQuery(path: string, query: any) {
+    function updateRouteParams(routeParams: any) {
+      self.routeParams.clear();
+      for (let key of Object.keys(routeParams)) {
+        self.routeParams.set(key, routeParams[key]);
+      }
+    }
+
+    function updatePathAndQuery(path: string, query: any, routeParams: any) {
       self.path = path;
       updateQuery(query);
+      updateRouteParams(routeParams);
     }
 
     return {

+ 2 - 0
public/test/mocks/common.ts

@@ -1,5 +1,6 @@
 export const backendSrv = {
   get: jest.fn(),
+  getDashboard: jest.fn(),
   post: jest.fn(),
 };
 
@@ -11,5 +12,6 @@ export function createNavTree(...args) {
     node.push(child);
     node = child.children;
   }
+
   return root;
 }