Browse Source

functions and tests

Peter Holmberg 7 years ago
parent
commit
94971abd9c

+ 51 - 0
public/app/features/users/UsersListPage.test.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { UsersListPage, Props } from './UsersListPage';
+import { NavModel, User } from 'app/types';
+import { getMockUser } from './__mocks__/userMocks';
+import appEvents from '../../core/app_events';
+
+jest.mock('../../core/app_events', () => ({
+  emit: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    navModel: {} as NavModel,
+    users: [] as User[],
+    searchQuery: '',
+    loadUsers: jest.fn(),
+    updateUser: jest.fn(),
+    removeUser: jest.fn(),
+    setUsersSearchQuery: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<UsersListPage {...props} />);
+  const instance = wrapper.instance() as UsersListPage;
+
+  return {
+    wrapper,
+    instance,
+  };
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const { wrapper } = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe('Functions', () => {
+  it('should emit show remove user modal', () => {
+    const { instance } = setup();
+    const mockUser = getMockUser();
+
+    instance.onRemoveUser(mockUser);
+
+    expect(appEvents.emit).toHaveBeenCalled();
+  });
+});

+ 30 - 2
public/app/features/users/UsersListPage.tsx

@@ -5,7 +5,8 @@ import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
 import UsersTable from 'app/features/users/UsersTable';
 import { NavModel, User } from 'app/types';
-import { loadUsers, setUsersSearchQuery } from './state/actions';
+import appEvents from 'app/core/app_events';
+import { loadUsers, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
 import { getNavModel } from '../../core/selectors/navModel';
 import { getUsers, getUsersSearchQuery } from './state/selectors';
 
@@ -15,6 +16,8 @@ export interface Props {
   searchQuery: string;
   loadUsers: typeof loadUsers;
   setUsersSearchQuery: typeof setUsersSearchQuery;
+  updateUser: typeof updateUser;
+  removeUser: typeof removeUser;
 }
 
 export class UsersListPage extends PureComponent<Props> {
@@ -25,6 +28,25 @@ export class UsersListPage extends PureComponent<Props> {
   async fetchUsers() {
     return await this.props.loadUsers();
   }
+
+  onRoleChange = (role, user) => {
+    const updatedUser = { ...user, role: role };
+
+    this.props.updateUser(updatedUser);
+  };
+
+  onRemoveUser = user => {
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: 'Are you sure you want to delete user ' + user.login + '?',
+      yesText: 'Delete',
+      icon: 'fa-warning',
+      onConfirm: () => {
+        this.props.removeUser(user.userId);
+      },
+    });
+  };
+
   render() {
     const { navModel, searchQuery, setUsersSearchQuery, users } = this.props;
 
@@ -43,7 +65,11 @@ export class UsersListPage extends PureComponent<Props> {
             setSearchQuery={setUsersSearchQuery}
             linkButton={linkButton}
           />
-          <UsersTable users={users} />
+          <UsersTable
+            users={users}
+            onRoleChange={(role, user) => this.onRoleChange(role, user)}
+            onRemoveUser={user => this.onRemoveUser(user)}
+          />
         </div>
       </div>
     );
@@ -61,6 +87,8 @@ function mapStateToProps(state) {
 const mapDispatchToProps = {
   loadUsers,
   setUsersSearchQuery,
+  updateUser,
+  removeUser,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage));

+ 33 - 0
public/app/features/users/UsersTable.test.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import UsersTable, { Props } from './UsersTable';
+import { User } from 'app/types';
+import { getMockUsers } from './__mocks__/userMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    users: [] as User[],
+    onRoleChange: jest.fn(),
+    onRemoveUser: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<UsersTable {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render users table', () => {
+    const wrapper = setup({
+      users: getMockUsers(5),
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 41 - 39
public/app/features/users/UsersTable.tsx

@@ -3,15 +3,15 @@ import { User } from 'app/types';
 
 export interface Props {
   users: User[];
-  onRoleChange: (value: string) => {};
+  onRoleChange: (role: string, user: User) => void;
+  onRemoveUser: (user: User) => void;
 }
 
 const UsersTable: SFC<Props> = props => {
-  const { users } = props;
+  const { users, onRoleChange, onRemoveUser } = props;
 
   return (
     <div>
-      Le Table
       <table className="filter-table form-inline">
         <thead>
           <tr>
@@ -23,42 +23,44 @@ const UsersTable: SFC<Props> = props => {
             <th style={{ width: '34px' }} />
           </tr>
         </thead>
-        {users.map((user, index) => {
-          return (
-            <tr key={`${user.userId}-${index}`}>
-              <td className="width-4 text-center">
-                <img className="filter-table__avatar" src={user.avatarUrl} />
-              </td>
-              <td>{user.login}</td>
-              <td>
-                <span className="ellipsis">{user.email}</span>
-              </td>
-              <td>{user.lastSeenAtAge}</td>
-              <td>
-                <div className="gf-form-select-wrapper width-12">
-                  <select
-                    value={user.role}
-                    className="gf-form-input"
-                    onChange={event => props.onRoleChange(event.target.value)}
-                  >
-                    {['Viewer', 'Editor', 'Admin'].map((option, index) => {
-                      return (
-                        <option value={option} key={`${option}-${index}`}>
-                          {option}
-                        </option>
-                      );
-                    })}
-                  </select>
-                </div>
-              </td>
-              <td>
-                <div onClick={() => props.removeUser(user)} className="btn btn-danger btn-mini">
-                  <i className="fa fa-remove" />
-                </div>
-              </td>
-            </tr>
-          );
-        })}
+        <tbody>
+          {users.map((user, index) => {
+            return (
+              <tr key={`${user.userId}-${index}`}>
+                <td className="width-4 text-center">
+                  <img className="filter-table__avatar" src={user.avatarUrl} />
+                </td>
+                <td>{user.login}</td>
+                <td>
+                  <span className="ellipsis">{user.email}</span>
+                </td>
+                <td>{user.lastSeenAtAge}</td>
+                <td>
+                  <div className="gf-form-select-wrapper width-12">
+                    <select
+                      value={user.role}
+                      className="gf-form-input"
+                      onChange={event => onRoleChange(event.target.value, user)}
+                    >
+                      {['Viewer', 'Editor', 'Admin'].map((option, index) => {
+                        return (
+                          <option value={option} key={`${option}-${index}`}>
+                            {option}
+                          </option>
+                        );
+                      })}
+                    </select>
+                  </div>
+                </td>
+                <td>
+                  <div onClick={() => onRemoveUser(user)} className="btn btn-danger btn-mini">
+                    <i className="fa fa-remove" />
+                  </div>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
       </table>
     </div>
   );

+ 31 - 0
public/app/features/users/__mocks__/userMocks.ts

@@ -0,0 +1,31 @@
+export const getMockUsers = (amount: number) => {
+  const users = [];
+
+  for (let i = 0; i <= amount; i++) {
+    users.push({
+      avatarUrl: 'url/to/avatar',
+      email: `user-${i}@test.com`,
+      lastSeenAt: '2018-10-01',
+      lastSeenAtAge: '',
+      login: `user-${i}`,
+      orgId: 1,
+      role: 'Admin',
+      userId: i,
+    });
+  }
+
+  return users;
+};
+
+export const getMockUser = () => {
+  return {
+    avatarUrl: 'url/to/avatar',
+    email: `user@test.com`,
+    lastSeenAt: '2018-10-01',
+    lastSeenAtAge: '',
+    login: `user`,
+    orgId: 1,
+    role: 'Admin',
+    userId: 2,
+  };
+};

+ 29 - 0
public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap

@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <OrgActionBar
+      linkButton={
+        Object {
+          "href": "/org/users/add",
+          "title": "Add user",
+        }
+      }
+      searchQuery=""
+      setSearchQuery={[MockFunction]}
+      showLayoutMode={false}
+    />
+    <UsersTable
+      onRemoveUser={[Function]}
+      onRoleChange={[Function]}
+      users={Array []}
+    />
+  </div>
+</div>
+`;

+ 448 - 0
public/app/features/users/__snapshots__/UsersTable.test.tsx.snap

@@ -0,0 +1,448 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div>
+  <table
+    className="filter-table form-inline"
+  >
+    <thead>
+      <tr>
+        <th />
+        <th>
+          Login
+        </th>
+        <th>
+          Email
+        </th>
+        <th>
+          Seen
+        </th>
+        <th>
+          Role
+        </th>
+        <th
+          style={
+            Object {
+              "width": "34px",
+            }
+          }
+        />
+      </tr>
+    </thead>
+    <tbody />
+  </table>
+</div>
+`;
+
+exports[`Render should render users table 1`] = `
+<div>
+  <table
+    className="filter-table form-inline"
+  >
+    <thead>
+      <tr>
+        <th />
+        <th>
+          Login
+        </th>
+        <th>
+          Email
+        </th>
+        <th>
+          Seen
+        </th>
+        <th>
+          Role
+        </th>
+        <th
+          style={
+            Object {
+              "width": "34px",
+            }
+          }
+        />
+      </tr>
+    </thead>
+    <tbody>
+      <tr
+        key="0-0"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-0
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-0@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+      <tr
+        key="1-1"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-1
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-1@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+      <tr
+        key="2-2"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-2
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-2@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+      <tr
+        key="3-3"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-3
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-3@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+      <tr
+        key="4-4"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-4
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-4@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+      <tr
+        key="5-5"
+      >
+        <td
+          className="width-4 text-center"
+        >
+          <img
+            className="filter-table__avatar"
+            src="url/to/avatar"
+          />
+        </td>
+        <td>
+          user-5
+        </td>
+        <td>
+          <span
+            className="ellipsis"
+          >
+            user-5@test.com
+          </span>
+        </td>
+        <td />
+        <td>
+          <div
+            className="gf-form-select-wrapper width-12"
+          >
+            <select
+              className="gf-form-input"
+              onChange={[Function]}
+              value="Admin"
+            >
+              <option
+                key="Viewer-0"
+                value="Viewer"
+              >
+                Viewer
+              </option>
+              <option
+                key="Editor-1"
+                value="Editor"
+              >
+                Editor
+              </option>
+              <option
+                key="Admin-2"
+                value="Admin"
+              >
+                Admin
+              </option>
+            </select>
+          </div>
+        </td>
+        <td>
+          <div
+            className="btn btn-danger btn-mini"
+            onClick={[Function]}
+          >
+            <i
+              className="fa fa-remove"
+            />
+          </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;

+ 14 - 0
public/app/features/users/state/actions.ts

@@ -38,3 +38,17 @@ export function loadUsers(): ThunkResult<void> {
     dispatch(usersLoaded(users));
   };
 }
+
+export function updateUser(user: User): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().patch(`/api/org/users/${user.userId}`, user);
+    dispatch(loadUsers());
+  };
+}
+
+export function removeUser(userId: number): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().delete(`/api/org/users/${userId}`);
+    dispatch(loadUsers());
+  };
+}