Bladeren bron

Folder pages to redux (#13235)

* creating types, actions, reducer

* load teams and store in redux

* delete team

* set search query action and tests

* Teampages page

* team members, bug in fetching team

* flattened team state, tests for TeamMembers

* test for team member selector

* wip: began folder to redux migration

* team settings

* actions for group sync

* wip: progress on redux folder store

* wip: folder to redux

* wip: folder settings page to redux progress

* mobx -> redux: major progress on folder migration

* redux: moved folders to it's own features folder

* fix: added loading nav states

* fix: gofmt issues

* wip: working on reducer test

* fix: added reducer test
Torkel Ödegaard 7 jaren geleden
bovenliggende
commit
f2edb82e79

+ 0 - 14
public/app/containers/ContainerProps.ts

@@ -1,14 +0,0 @@
-import { NavStore } from './../stores/NavStore/NavStore';
-import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore';
-import { ViewStore } from './../stores/ViewStore/ViewStore';
-import { FolderStore } from './../stores/FolderStore/FolderStore';
-
-interface ContainerProps {
-  nav: typeof NavStore.Type;
-  permissions: typeof PermissionsStore.Type;
-  view: typeof ViewStore.Type;
-  folder: typeof FolderStore.Type;
-  backendSrv: any;
-}
-
-export default ContainerProps;

+ 0 - 84
public/app/containers/ManageDashboards/FolderSettings.test.tsx

@@ -1,84 +0,0 @@
-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.getFolderByUid.mockReturnValue(
-      Promise.resolve({
-        id: 1,
-        uid: 'uid',
-        title: 'Folder Name',
-        url: '/dashboards/f/uid/folder-name',
-        canSave: true,
-        version: 1,
-      })
-    );
-
-    const store = RootStore.create(
-      {
-        view: {
-          path: 'asd',
-          query: {},
-          routeParams: {
-            uid: 'uid-str',
-          },
-        },
-      },
-      {
-        backendSrv: backendSrv,
-      }
-    );
-
-    wrapper = shallow(<FolderSettings backendSrv={backendSrv} {...store} />);
-    page = wrapper.dive();
-    return page
-      .instance()
-      .loadStore()
-      .then(() => {
-        page.update();
-      });
-  });
-
-  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);
-  });
-});

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

@@ -1,160 +0,0 @@
-import React from 'react';
-import { hot } from 'react-hot-loader';
-import { inject, observer } from 'mobx-react';
-import { toJS } from 'mobx';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import ContainerProps from 'app/containers/ContainerProps';
-import { getSnapshot } from 'mobx-state-tree';
-import appEvents from 'app/core/app_events';
-
-@inject('nav', 'folder', 'view')
-@observer
-export class FolderSettings extends React.Component<ContainerProps, any> {
-  formSnapshot: any;
-
-  componentDidMount() {
-    this.loadStore();
-  }
-
-  loadStore() {
-    const { nav, folder, view } = this.props;
-
-    return folder.load(view.routeParams.get('uid') as string).then(res => {
-      this.formSnapshot = getSnapshot(folder);
-      view.updatePathAndQuery(`${res.url}/settings`, {}, {});
-
-      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
-      .saveFolder({ 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.bind(this));
-  }
-
-  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 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;
-
-      const { nav, folder, view } = this.props;
-
-      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: () => {
-          folder
-            .saveFolder({ overwrite: true })
-            .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');
-            });
-        },
-      });
-    }
-  }
-
-  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-save" /> 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>
-    );
-  }
-}
-
-export default hot(module)(FolderSettings);

+ 2 - 2
public/app/core/reducers/location.ts

@@ -9,8 +9,8 @@ export const initialState: LocationState = {
   routeParams: {},
 };
 
-function renderUrl(path: string, query: UrlQueryMap): string {
-  if (Object.keys(query).length > 0) {
+function renderUrl(path: string, query: UrlQueryMap | undefined): string {
+  if (query && Object.keys(query).length > 0) {
     path += '?' + toUrlParams(query);
   }
   return path;

+ 7 - 3
public/app/core/selectors/navModel.ts

@@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel {
   };
 }
 
-export function getNavModel(navIndex: NavIndex, id: string): NavModel {
+export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel {
   if (navIndex[id]) {
     const node = navIndex[id];
     const main = {
@@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel {
       node: node,
       main: main,
     };
-  } else {
-    return getNotFoundModel();
   }
+
+  if (fallback) {
+    return fallback;
+  }
+
+  return getNotFoundModel();
 }

+ 0 - 10
public/app/core/services/backend_srv.ts

@@ -252,16 +252,6 @@ export class BackendSrv {
     return this.post('/api/folders', payload);
   }
 
-  updateFolder(folder, options) {
-    options = options || {};
-
-    return this.put(`/api/folders/${folder.uid}`, {
-      title: folder.title,
-      version: folder.version,
-      overwrite: options.overwrite === true,
-    });
-  }
-
   deleteFolder(uid: string, showSuccessAlert) {
     return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
   }

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

@@ -32,11 +32,9 @@ import './dashlinks/module';
 
 import coreModule from 'app/core/core_module';
 import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
-import { FolderSettingsCtrl } from './folder_settings_ctrl';
 import { DashboardImportCtrl } from './dashboard_import_ctrl';
 import { CreateFolderCtrl } from './create_folder_ctrl';
 
 coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
-coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl);
 coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
 coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);

+ 0 - 94
public/app/features/dashboard/folder_settings_ctrl.ts

@@ -1,94 +0,0 @@
-import { FolderPageLoader } from './folder_page_loader';
-import appEvents from 'app/core/app_events';
-
-export class FolderSettingsCtrl {
-  folderPageLoader: FolderPageLoader;
-  navModel: any;
-  folderId: number;
-  uid: string;
-  canSave = false;
-  folder: any;
-  title: string;
-  hasChanged: boolean;
-
-  /** @ngInject */
-  constructor(private backendSrv, navModelSrv, private $routeParams, private $location) {
-    if (this.$routeParams.uid) {
-      this.uid = $routeParams.uid;
-
-      this.folderPageLoader = new FolderPageLoader(this.backendSrv);
-      this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => {
-        if ($location.path() !== folder.meta.url) {
-          $location.path(`${folder.meta.url}/settings`).replace();
-        }
-
-        this.folder = folder;
-        this.canSave = this.folder.canSave;
-        this.title = this.folder.title;
-      });
-    }
-  }
-
-  save() {
-    this.titleChanged();
-
-    if (!this.hasChanged) {
-      return;
-    }
-
-    this.folder.title = this.title.trim();
-
-    return this.backendSrv
-      .updateFolder(this.folder)
-      .then(result => {
-        if (result.url !== this.$location.path()) {
-          this.$location.url(result.url + '/settings');
-        }
-
-        appEvents.emit('dashboard-saved');
-        appEvents.emit('alert-success', ['Folder saved']);
-      })
-      .catch(this.handleSaveFolderError);
-  }
-
-  titleChanged() {
-    this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase();
-  }
-
-  delete(evt) {
-    if (evt) {
-      evt.stopPropagation();
-      evt.preventDefault();
-    }
-
-    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.backendSrv.deleteFolder(this.uid).then(() => {
-          appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]);
-          this.$location.url('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.backendSrv.updateFolder(this.folder, { overwrite: true });
-        },
-      });
-    }
-  }
-}

+ 37 - 19
public/app/containers/ManageDashboards/FolderPermissions.tsx → public/app/features/folders/FolderPermissions.tsx

@@ -1,25 +1,38 @@
 import React, { Component } from 'react';
 import { hot } from 'react-hot-loader';
 import { inject, observer } from 'mobx-react';
-import { toJS } from 'mobx';
-import ContainerProps from 'app/containers/ContainerProps';
+import { connect } from 'react-redux';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import Permissions from 'app/core/components/Permissions/Permissions';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
 import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
 import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import SlideDown from 'app/core/components/Animations/SlideDown';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { NavModel, StoreState, FolderState } from 'app/types';
+import { getFolderByUid } from './state/actions';
+import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore';
+import { getLoadingNav } from './state/navModel';
 
-@inject('nav', 'folder', 'view', 'permissions')
+export interface Props {
+  navModel: NavModel;
+  getFolderByUid: typeof getFolderByUid;
+  folderUid: string;
+  folder: FolderState;
+  permissions: typeof PermissionsStore.Type;
+  backendSrv: any;
+}
+
+@inject('permissions')
 @observer
-export class FolderPermissions extends Component<ContainerProps, any> {
+export class FolderPermissions extends Component<Props> {
   constructor(props) {
     super(props);
     this.handleAddPermission = this.handleAddPermission.bind(this);
   }
 
   componentDidMount() {
-    this.loadStore();
+    this.props.getFolderByUid(this.props.folderUid);
   }
 
   componentWillUnmount() {
@@ -27,31 +40,23 @@ export class FolderPermissions extends Component<ContainerProps, any> {
     permissions.hideAddPermissions();
   }
 
-  loadStore() {
-    const { nav, folder, view } = this.props;
-    return folder.load(view.routeParams.get('uid') as string).then(res => {
-      view.updatePathAndQuery(`${res.url}/permissions`, {}, {});
-      return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
-    });
-  }
-
   handleAddPermission() {
     const { permissions } = this.props;
     permissions.toggleAddPermissions();
   }
 
   render() {
-    const { nav, folder, permissions, backendSrv } = this.props;
+    const { navModel, permissions, backendSrv, folder } = this.props;
 
-    if (!folder.folder || !nav.main) {
-      return <h2>Loading</h2>;
+    if (folder.id === 0) {
+      return <PageHeader model={navModel} />;
     }
 
-    const dashboardId = folder.folder.id;
+    const dashboardId = folder.id;
 
     return (
       <div>
-        <PageHeader model={nav as any} />
+        <PageHeader model={navModel} />
         <div className="page-container page-body">
           <div className="page-action-bar">
             <h3 className="page-sub-heading">Folder Permissions</h3>
@@ -77,4 +82,17 @@ export class FolderPermissions extends Component<ContainerProps, any> {
   }
 }
 
-export default hot(module)(FolderPermissions);
+const mapStateToProps = (state: StoreState) => {
+  const uid = state.location.routeParams.uid;
+  return {
+    navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)),
+    folderUid: uid,
+    folder: state.folder,
+  };
+};
+
+const mapDispatchToProps = {
+  getFolderByUid,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));

+ 55 - 0
public/app/features/folders/FolderSettingsPage.test.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import { FolderSettingsPage, Props } from './FolderSettingsPage';
+import { NavModel } from 'app/types';
+import { shallow } from 'enzyme';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    navModel: {} as NavModel,
+    folderUid: '1234',
+    folder: {
+      id: 0,
+      uid: '1234',
+      title: 'loading',
+      canSave: true,
+      url: 'url',
+      hasChanged: false,
+      version: 1,
+    },
+    getFolderByUid: jest.fn(),
+    setFolderTitle: jest.fn(),
+    saveFolder: jest.fn(),
+    deleteFolder: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<FolderSettingsPage {...props} />);
+  const instance = wrapper.instance() as FolderSettingsPage;
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should enable save button', () => {
+    const { wrapper } = setup({
+      folder: {
+        id: 1,
+        uid: '1234',
+        title: 'loading',
+        canSave: true,
+        hasChanged: true,
+        version: 1,
+      },
+    });
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 105 - 0
public/app/features/folders/FolderSettingsPage.tsx

@@ -0,0 +1,105 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import appEvents from 'app/core/app_events';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { NavModel, StoreState, FolderState } from 'app/types';
+import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions';
+import { getLoadingNav } from './state/navModel';
+
+export interface Props {
+  navModel: NavModel;
+  folderUid: string;
+  folder: FolderState;
+  getFolderByUid: typeof getFolderByUid;
+  setFolderTitle: typeof setFolderTitle;
+  saveFolder: typeof saveFolder;
+  deleteFolder: typeof deleteFolder;
+}
+
+export class FolderSettingsPage extends PureComponent<Props> {
+  componentDidMount() {
+    this.props.getFolderByUid(this.props.folderUid);
+  }
+
+  onTitleChange = evt => {
+    this.props.setFolderTitle(evt.target.value);
+  };
+
+  onSave = async evt => {
+    evt.preventDefault();
+    evt.stopPropagation();
+
+    await this.props.saveFolder(this.props.folder);
+  };
+
+  onDelete = evt => {
+    evt.stopPropagation();
+    evt.preventDefault();
+
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: `Do you want to delete this folder and all its dashboards?`,
+      icon: 'fa-trash',
+      yesText: 'Delete',
+      onConfirm: () => {
+        this.props.deleteFolder(this.props.folder.uid);
+      },
+    });
+  };
+
+  render() {
+    const { navModel, folder } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <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.onSave}>
+              <div className="gf-form">
+                <label className="gf-form-label width-7">Name</label>
+                <input
+                  type="text"
+                  className="gf-form-input width-30"
+                  value={folder.title}
+                  onChange={this.onTitleChange}
+                />
+              </div>
+              <div className="gf-form-button-row">
+                <button type="submit" className="btn btn-success" disabled={!folder.canSave || !folder.hasChanged}>
+                  <i className="fa fa-save" /> Save
+                </button>
+                <button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
+                  <i className="fa fa-trash" /> Delete
+                </button>
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: StoreState) => {
+  const uid = state.location.routeParams.uid;
+
+  return {
+    navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)),
+    folderUid: uid,
+    folder: state.folder,
+  };
+};
+
+const mapDispatchToProps = {
+  getFolderByUid,
+  saveFolder,
+  setFolderTitle,
+  deleteFolder,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage));

+ 131 - 0
public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap

@@ -0,0 +1,131 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should enable save button 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <h2
+      className="page-sub-heading"
+    >
+      Folder Settings
+    </h2>
+    <div
+      className="section gf-form-group"
+    >
+      <form
+        name="folderSettingsForm"
+        onSubmit={[Function]}
+      >
+        <div
+          className="gf-form"
+        >
+          <label
+            className="gf-form-label width-7"
+          >
+            Name
+          </label>
+          <input
+            className="gf-form-input width-30"
+            onChange={[Function]}
+            type="text"
+            value="loading"
+          />
+        </div>
+        <div
+          className="gf-form-button-row"
+        >
+          <button
+            className="btn btn-success"
+            disabled={false}
+            type="submit"
+          >
+            <i
+              className="fa fa-save"
+            />
+             Save
+          </button>
+          <button
+            className="btn btn-danger"
+            disabled={false}
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-trash"
+            />
+             Delete
+          </button>
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <h2
+      className="page-sub-heading"
+    >
+      Folder Settings
+    </h2>
+    <div
+      className="section gf-form-group"
+    >
+      <form
+        name="folderSettingsForm"
+        onSubmit={[Function]}
+      >
+        <div
+          className="gf-form"
+        >
+          <label
+            className="gf-form-label width-7"
+          >
+            Name
+          </label>
+          <input
+            className="gf-form-input width-30"
+            onChange={[Function]}
+            type="text"
+            value="loading"
+          />
+        </div>
+        <div
+          className="gf-form-button-row"
+        >
+          <button
+            className="btn btn-success"
+            disabled={true}
+            type="submit"
+          >
+            <i
+              className="fa fa-save"
+            />
+             Save
+          </button>
+          <button
+            className="btn btn-danger"
+            disabled={false}
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-trash"
+            />
+             Delete
+          </button>
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+`;

+ 67 - 0
public/app/features/folders/state/actions.ts

@@ -0,0 +1,67 @@
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { StoreState } from 'app/types';
+import { ThunkAction } from 'redux-thunk';
+import { FolderDTO, FolderState } from 'app/types';
+import { updateNavIndex, updateLocation } from 'app/core/actions';
+import { buildNavModel } from './navModel';
+import appEvents from 'app/core/app_events';
+
+export enum ActionTypes {
+  LoadFolder = 'LOAD_FOLDER',
+  SetFolderTitle = 'SET_FOLDER_TITLE',
+  SaveFolder = 'SAVE_FOLDER',
+}
+
+export interface LoadFolderAction {
+  type: ActionTypes.LoadFolder;
+  payload: FolderDTO;
+}
+
+export interface SetFolderTitleAction {
+  type: ActionTypes.SetFolderTitle;
+  payload: string;
+}
+
+export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
+  type: ActionTypes.LoadFolder,
+  payload: folder,
+});
+
+export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({
+  type: ActionTypes.SetFolderTitle,
+  payload: newTitle,
+});
+
+export type Action = LoadFolderAction | SetFolderTitleAction;
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
+
+export function getFolderByUid(uid: string): ThunkResult<void> {
+  return async dispatch => {
+    const folder = await getBackendSrv().getFolderByUid(uid);
+    dispatch(loadFolder(folder));
+    dispatch(updateNavIndex(buildNavModel(folder)));
+  };
+}
+
+export function saveFolder(folder: FolderState): ThunkResult<void> {
+  return async dispatch => {
+    const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, {
+      title: folder.title,
+      version: folder.version,
+    });
+
+    // this should be redux action at some point
+    appEvents.emit('alert-success', ['Folder saved']);
+
+    dispatch(updateLocation({ path: `${res.url}/settings` }));
+  };
+}
+
+export function deleteFolder(uid: string): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().deleteFolder(uid, true);
+    dispatch(updateLocation({ path: `dashboards` }));
+  };
+}

+ 53 - 0
public/app/features/folders/state/navModel.ts

@@ -0,0 +1,53 @@
+import { FolderDTO, NavModelItem, NavModel } from 'app/types';
+
+export function buildNavModel(folder: FolderDTO): NavModelItem {
+  return {
+    icon: 'fa fa-folder-open',
+    id: 'manage-folder',
+    subTitle: 'Manage folder dashboards & permissions',
+    url: '',
+    text: folder.title,
+    breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }],
+    children: [
+      {
+        active: false,
+        icon: 'fa fa-fw fa-th-large',
+        id: `folder-dashboards-${folder.uid}`,
+        text: 'Dashboards',
+        url: folder.url,
+      },
+      {
+        active: false,
+        icon: 'fa fa-fw fa-lock',
+        id: `folder-permissions-${folder.uid}`,
+        text: 'Permissions',
+        url: `${folder.url}/permissions`,
+      },
+      {
+        active: false,
+        icon: 'fa fa-fw fa-cog',
+        id: `folder-settings-${folder.uid}`,
+        text: 'Settings',
+        url: `${folder.url}/settings`,
+      },
+    ],
+  };
+}
+
+export function getLoadingNav(tabIndex: number): NavModel {
+  const main = buildNavModel({
+    id: 1,
+    uid: 'loading',
+    title: 'Loading',
+    url: 'url',
+    canSave: false,
+    version: 0,
+  });
+
+  main.children[tabIndex].active = true;
+
+  return {
+    main: main,
+    node: main.children[tabIndex],
+  };
+}

+ 42 - 0
public/app/features/folders/state/reducers.test.ts

@@ -0,0 +1,42 @@
+import { Action, ActionTypes } from './actions';
+import { FolderDTO } from 'app/types';
+import { inititalState, folderReducer } from './reducers';
+
+function getTestFolder(): FolderDTO {
+  return {
+    id: 1,
+    title: 'test folder',
+    uid: 'asd',
+    url: 'url',
+    canSave: true,
+    version: 0,
+  };
+}
+
+describe('folder reducer', () => {
+  it('should load folder and set hasChanged to false', () => {
+    const folder = getTestFolder();
+
+    const action: Action = {
+      type: ActionTypes.LoadFolder,
+      payload: folder,
+    };
+
+    const state = folderReducer(inititalState, action);
+
+    expect(state.hasChanged).toEqual(false);
+    expect(state.title).toEqual('test folder');
+  });
+
+  it('should set title', () => {
+    const action: Action = {
+      type: ActionTypes.SetFolderTitle,
+      payload: 'new title',
+    };
+
+    const state = folderReducer(inititalState, action);
+
+    expect(state.hasChanged).toEqual(true);
+    expect(state.title).toEqual('new title');
+  });
+});

+ 33 - 0
public/app/features/folders/state/reducers.ts

@@ -0,0 +1,33 @@
+import { FolderState } from 'app/types';
+import { Action, ActionTypes } from './actions';
+
+export const inititalState: FolderState = {
+  id: 0,
+  uid: 'loading',
+  title: 'loading',
+  url: '',
+  canSave: false,
+  hasChanged: false,
+  version: 0,
+};
+
+export const folderReducer = (state = inititalState, action: Action): FolderState => {
+  switch (action.type) {
+    case ActionTypes.LoadFolder:
+      return {
+        ...action.payload,
+        hasChanged: false,
+      };
+    case ActionTypes.SetFolderTitle:
+      return {
+        ...state,
+        title: action.payload,
+        hasChanged: action.payload.trim().length > 0,
+      };
+  }
+  return state;
+};
+
+export default {
+  folder: folderReducer,
+};

+ 5 - 3
public/app/features/teams/TeamPages.tsx

@@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import TeamMembers from './TeamMembers';
 import TeamSettings from './TeamSettings';
 import TeamGroupSync from './TeamGroupSync';
-import { NavModel, Team } from '../../types';
+import { NavModel, Team } from 'app/types';
 import { loadTeam } from './state/actions';
 import { getTeam } from './state/selectors';
-import { getNavModel } from '../../core/selectors/navModel';
+import { getTeamLoadingNav } from './state/navModel';
+import { getNavModel } from 'app/core/selectors/navModel';
 import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location';
 
 export interface Props {
@@ -89,9 +90,10 @@ export class TeamPages extends PureComponent<Props, State> {
 function mapStateToProps(state) {
   const teamId = getRouteParamsId(state.location);
   const pageName = getRouteParamsPage(state.location) || 'members';
+  const teamLoadingNav = getTeamLoadingNav(pageName);
 
   return {
-    navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`),
+    navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav),
     teamId: teamId,
     pageName: pageName,
     team: getTeam(state.team, teamId),

+ 21 - 96
public/app/features/teams/state/actions.ts

@@ -1,8 +1,8 @@
 import { ThunkAction } from 'redux-thunk';
 import { getBackendSrv } from 'app/core/services/backend_srv';
-import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types';
+import { StoreState, Team, TeamGroup, TeamMember } from 'app/types';
 import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
-import config from 'app/core/config';
+import { buildNavModel } from './navModel';
 
 export enum ActionTypes {
   LoadTeams = 'LOAD_TEAMS',
@@ -90,148 +90,73 @@ export function loadTeams(): ThunkResult<void> {
   };
 }
 
-function buildNavModel(team: Team): NavModelItem {
-  const navModel = {
-    img: team.avatarUrl,
-    id: 'team-' + team.id,
-    subTitle: 'Manage members & settings',
-    url: '',
-    text: team.name,
-    breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
-    children: [
-      {
-        active: false,
-        icon: 'gicon gicon-team',
-        id: `team-members-${team.id}`,
-        text: 'Members',
-        url: `org/teams/edit/${team.id}/members`,
-      },
-      {
-        active: false,
-        icon: 'fa fa-fw fa-sliders',
-        id: `team-settings-${team.id}`,
-        text: 'Settings',
-        url: `org/teams/edit/${team.id}/settings`,
-      },
-    ],
-  };
-
-  if (config.buildInfo.isEnterprise) {
-    navModel.children.push({
-      active: false,
-      icon: 'fa fa-fw fa-refresh',
-      id: `team-groupsync-${team.id}`,
-      text: 'External group sync',
-      url: `org/teams/edit/${team.id}/groupsync`,
-    });
-  }
-
-  return navModel;
-}
-
 export function loadTeam(id: number): ThunkResult<void> {
   return async dispatch => {
-    await getBackendSrv()
-      .get(`/api/teams/${id}`)
-      .then(response => {
-        dispatch(teamLoaded(response));
-        dispatch(updateNavIndex(buildNavModel(response)));
-      });
+    const response = await getBackendSrv().get(`/api/teams/${id}`);
+    dispatch(teamLoaded(response));
+    dispatch(updateNavIndex(buildNavModel(response)));
   };
 }
 
 export function loadTeamMembers(): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .get(`/api/teams/${team.id}/members`)
-      .then(response => {
-        dispatch(teamMembersLoaded(response));
-      });
+    const response = await getBackendSrv().get(`/api/teams/${team.id}/members`);
+    dispatch(teamMembersLoaded(response));
   };
 }
 
 export function addTeamMember(id: number): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .post(`/api/teams/${team.id}/members`, { userId: id })
-      .then(() => {
-        dispatch(loadTeamMembers());
-      });
+    await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id });
+    dispatch(loadTeamMembers());
   };
 }
 
 export function removeTeamMember(id: number): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .delete(`/api/teams/${team.id}/members/${id}`)
-      .then(() => {
-        dispatch(loadTeamMembers());
-      });
+    await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`);
+    dispatch(loadTeamMembers());
   };
 }
 
 export function updateTeam(name: string, email: string): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-    await getBackendSrv()
-      .put(`/api/teams/${team.id}`, {
-        name,
-        email,
-      })
-      .then(() => {
-        dispatch(loadTeam(team.id));
-      });
+    await getBackendSrv().put(`/api/teams/${team.id}`, { name, email });
+    dispatch(loadTeam(team.id));
   };
 }
 
 export function loadTeamGroups(): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .get(`/api/teams/${team.id}/groups`)
-      .then(response => {
-        dispatch(teamGroupsLoaded(response));
-      });
+    const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`);
+    dispatch(teamGroupsLoaded(response));
   };
 }
 
 export function addTeamGroup(groupId: string): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .post(`/api/teams/${team.id}/groups`, { groupId: groupId })
-      .then(() => {
-        dispatch(loadTeamGroups());
-      });
+    await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId });
+    dispatch(loadTeamGroups());
   };
 }
 
 export function removeTeamGroup(groupId: string): ThunkResult<void> {
   return async (dispatch, getStore) => {
     const team = getStore().team.team;
-
-    await getBackendSrv()
-      .delete(`/api/teams/${team.id}/groups/${groupId}`)
-      .then(() => {
-        dispatch(loadTeamGroups());
-      });
+    await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`);
+    dispatch(loadTeamGroups());
   };
 }
 
 export function deleteTeam(id: number): ThunkResult<void> {
   return async dispatch => {
-    await getBackendSrv()
-      .delete(`/api/teams/${id}`)
-      .then(() => {
-        dispatch(loadTeams());
-      });
+    await getBackendSrv().delete(`/api/teams/${id}`);
+    dispatch(loadTeams());
   };
 }

+ 67 - 0
public/app/features/teams/state/navModel.ts

@@ -0,0 +1,67 @@
+import { Team, NavModelItem, NavModel } from 'app/types';
+import config from 'app/core/config';
+
+export function buildNavModel(team: Team): NavModelItem {
+  const navModel = {
+    img: team.avatarUrl,
+    id: 'team-' + team.id,
+    subTitle: 'Manage members & settings',
+    url: '',
+    text: team.name,
+    breadcrumbs: [{ title: 'Teams', url: 'org/teams' }],
+    children: [
+      {
+        active: false,
+        icon: 'gicon gicon-team',
+        id: `team-members-${team.id}`,
+        text: 'Members',
+        url: `org/teams/edit/${team.id}/members`,
+      },
+      {
+        active: false,
+        icon: 'fa fa-fw fa-sliders',
+        id: `team-settings-${team.id}`,
+        text: 'Settings',
+        url: `org/teams/edit/${team.id}/settings`,
+      },
+    ],
+  };
+
+  if (config.buildInfo.isEnterprise) {
+    navModel.children.push({
+      active: false,
+      icon: 'fa fa-fw fa-refresh',
+      id: `team-groupsync-${team.id}`,
+      text: 'External group sync',
+      url: `org/teams/edit/${team.id}/groupsync`,
+    });
+  }
+
+  return navModel;
+}
+
+export function getTeamLoadingNav(pageName: string): NavModel {
+  const main = buildNavModel({
+    avatarUrl: 'public/img/user_profile.png',
+    id: 1,
+    name: 'Loading',
+    email: 'loading',
+    memberCount: 0,
+  });
+
+  let node: NavModelItem;
+
+  // find active page
+  for (const child of main.children) {
+    if (child.id.indexOf(pageName) > 0) {
+      child.active = true;
+      node = child;
+      break;
+    }
+  }
+
+  return {
+    main: main,
+    node: node,
+  };
+}

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

@@ -3,10 +3,10 @@ import './ReactContainer';
 
 import ServerStats from 'app/features/admin/ServerStats';
 import AlertRuleList from 'app/features/alerting/AlertRuleList';
-import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions';
 import TeamPages from 'app/features/teams/TeamPages';
 import TeamList from 'app/features/teams/TeamList';
-import FolderSettings from 'app/containers/ManageDashboards/FolderSettings';
+import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
+import FolderPermissions from 'app/features/folders/FolderPermissions';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
     .when('/dashboards/f/:uid/:slug/settings', {
       template: '<react-container />',
       resolve: {
-        component: () => FolderSettings,
+        component: () => FolderSettingsPage,
       },
     })
     .when('/dashboards/f/:uid/:slug', {

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

@@ -1,60 +0,0 @@
-import { types, getEnv, flow } from 'mobx-state-tree';
-
-export const Folder = types.model('Folder', {
-  id: types.identifier(types.number),
-  uid: types.string,
-  title: types.string,
-  url: types.string,
-  canSave: types.boolean,
-  hasChanged: types.boolean,
-  version: types.number,
-});
-
-export const FolderStore = types
-  .model('FolderStore', {
-    folder: types.maybe(Folder),
-  })
-  .actions(self => ({
-    load: flow(function* load(uid: string) {
-      // clear folder state
-      if (self.folder && self.folder.uid !== uid) {
-        self.folder = null;
-      }
-
-      const backendSrv = getEnv(self).backendSrv;
-      const res = yield backendSrv.getFolderByUid(uid);
-      self.folder = Folder.create({
-        id: res.id,
-        uid: res.uid,
-        title: res.title,
-        url: res.url,
-        canSave: res.canSave,
-        hasChanged: false,
-        version: res.version,
-      });
-
-      return res;
-    }),
-
-    setTitle: (originalTitle: string, title: string) => {
-      self.folder.title = title;
-      self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0;
-    },
-
-    saveFolder: flow(function* saveFolder(options: any) {
-      const backendSrv = getEnv(self).backendSrv;
-      self.folder.title = self.folder.title.trim();
-
-      const res = yield backendSrv.updateFolder(self.folder, options);
-      self.folder.url = res.url;
-      self.folder.version = res.version;
-
-      return `${self.folder.url}/settings`;
-    }),
-
-    deleteFolder: flow(function* deleteFolder() {
-      const backendSrv = getEnv(self).backendSrv;
-
-      return backendSrv.deleteFolder(self.folder.uid);
-    }),
-  }));

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

@@ -1,7 +1,6 @@
 import { types } from 'mobx-state-tree';
 import { NavStore } from './../NavStore/NavStore';
 import { ViewStore } from './../ViewStore/ViewStore';
-import { FolderStore } from './../FolderStore/FolderStore';
 import { PermissionsStore } from './../PermissionsStore/PermissionsStore';
 
 export const RootStore = types.model({
@@ -15,7 +14,6 @@ export const RootStore = types.model({
     query: {},
     routeParams: {},
   }),
-  folder: types.optional(FolderStore, {}),
 });
 
 type RootStoreType = typeof RootStore.Type;

+ 2 - 0
public/app/stores/configureStore.ts

@@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger';
 import sharedReducers from 'app/core/reducers';
 import alertingReducers from 'app/features/alerting/state/reducers';
 import teamsReducers from 'app/features/teams/state/reducers';
+import foldersReducers from 'app/features/folders/state/reducers';
 
 const rootReducer = combineReducers({
   ...sharedReducers,
   ...alertingReducers,
   ...teamsReducers,
+  ...foldersReducers,
 });
 
 export let store;

+ 18 - 0
public/app/types/folder.ts

@@ -0,0 +1,18 @@
+export interface FolderDTO {
+  id: number;
+  uid: string;
+  title: string;
+  url: string;
+  version: number;
+  canSave: boolean;
+}
+
+export interface FolderState {
+  id: number;
+  uid: string;
+  title: string;
+  url: string;
+  version: number;
+  canSave: boolean;
+  hasChanged: boolean;
+}

+ 4 - 0
public/app/types/index.ts

@@ -2,6 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams';
 import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
 import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
 import { NavModel, NavModelItem, NavIndex } from './navModel';
+import { FolderDTO, FolderState } from './folder';
 
 export {
   Team,
@@ -19,6 +20,8 @@ export {
   NavIndex,
   UrlQueryMap,
   UrlQueryValue,
+  FolderDTO,
+  FolderState,
 };
 
 export interface StoreState {
@@ -27,4 +30,5 @@ export interface StoreState {
   alertRules: AlertRulesState;
   teams: TeamsState;
   team: TeamState;
+  folder: FolderState;
 }

+ 4 - 26
yarn.lock

@@ -3182,7 +3182,7 @@ debug@^3.1.0:
   dependencies:
     ms "^2.1.1"
 
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
 
@@ -5553,7 +5553,7 @@ import-local@^2.0.0:
     pkg-dir "^3.0.0"
     resolve-cwd "^2.0.0"
 
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
 
@@ -6990,10 +6990,6 @@ lodash-es@^4.17.5:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
 
-lodash._baseindexof@*:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
-
 lodash._baseuniq@~4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0:
     lodash._createset "~4.0.0"
     lodash._root "~3.0.0"
 
-lodash._bindcallback@*:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
-
-lodash._cacheindexof@*:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
-
-lodash._createcache@*:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
-  dependencies:
-    lodash._getnative "^3.0.0"
-
 lodash._createset@~4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
 
-lodash._getnative@*, lodash._getnative@^3.0.0:
+lodash._getnative@^3.0.0:
   version "3.9.1"
   resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
 
@@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
 
-lodash.restparam@*:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
-
 lodash.sortby@^4.7.0:
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -9902,7 +9880,7 @@ readable-stream@~1.1.10:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
   dependencies: