Browse Source

first crude display

Peter Holmberg 7 năm trước cách đây
mục cha
commit
8f99276606

+ 7 - 4
public/app/core/components/OrgActionBar/OrgActionBar.tsx

@@ -3,15 +3,16 @@ import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
 
 export interface Props {
   searchQuery: string;
-  layoutMode: LayoutMode;
-  setLayoutMode: (mode: LayoutMode) => {};
+  layoutMode?: LayoutMode;
+  showLayoutMode: boolean;
+  setLayoutMode?: (mode: LayoutMode) => {};
   setSearchQuery: (value: string) => {};
   linkButton: { href: string; title: string };
 }
 
 export default class OrgActionBar extends PureComponent<Props> {
   render() {
-    const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery } = this.props;
+    const { searchQuery, layoutMode, setLayoutMode, linkButton, setSearchQuery, showLayoutMode } = this.props;
 
     return (
       <div className="page-action-bar">
@@ -26,7 +27,9 @@ export default class OrgActionBar extends PureComponent<Props> {
             />
             <i className="gf-form-input-icon fa fa-search" />
           </label>
-          <LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} />
+          {showLayoutMode && (
+            <LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} />
+          )}
         </div>
         <div className="page-action-bar__spacer" />
         <a className="btn btn-success" href={linkButton.href} target="_blank">

+ 1 - 1
public/app/features/plugins/PluginListPage.test.tsx

@@ -10,7 +10,7 @@ const setup = (propOverrides?: object) => {
     plugins: [] as Plugin[],
     searchQuery: '',
     setPluginsSearchQuery: jest.fn(),
-    setPluginsLayoutMoode: jest.fn(),
+    setPluginsLayoutMode: jest.fn(),
     layoutMode: LayoutModes.Grid,
     loadPlugins: jest.fn(),
   };

+ 4 - 3
public/app/features/plugins/PluginListPage.tsx

@@ -16,7 +16,7 @@ export interface Props {
   layoutMode: LayoutMode;
   searchQuery: string;
   loadPlugins: typeof loadPlugins;
-  setPluginsLayoutMoode: typeof setPluginsLayoutMode;
+  setPluginsLayoutMode: typeof setPluginsLayoutMode;
   setPluginsSearchQuery: typeof setPluginsSearchQuery;
 }
 
@@ -30,7 +30,7 @@ export class PluginListPage extends PureComponent<Props> {
   }
 
   render() {
-    const { navModel, plugins, layoutMode, setPluginsLayoutMoode, setPluginsSearchQuery, searchQuery } = this.props;
+    const { navModel, plugins, layoutMode, setPluginsLayoutMode, setPluginsSearchQuery, searchQuery } = this.props;
 
     const linkButton = {
       href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
@@ -42,8 +42,9 @@ export class PluginListPage extends PureComponent<Props> {
         <div className="page-container page-body">
           <OrgActionBar
             searchQuery={searchQuery}
+            showLayoutMode={true}
             layoutMode={layoutMode}
-            setLayoutMode={mode => setPluginsLayoutMoode(mode)}
+            setLayoutMode={mode => setPluginsLayoutMode(mode)}
             setSearchQuery={query => setPluginsSearchQuery(query)}
             linkButton={linkButton}
           />

+ 66 - 0
public/app/features/users/UsersListPage.tsx

@@ -0,0 +1,66 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+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 { getNavModel } from '../../core/selectors/navModel';
+import { getUsers, getUsersSearchQuery } from './state/selectors';
+
+export interface Props {
+  navModel: NavModel;
+  users: User[];
+  searchQuery: string;
+  loadUsers: typeof loadUsers;
+  setUsersSearchQuery: typeof setUsersSearchQuery;
+}
+
+export class UsersListPage extends PureComponent<Props> {
+  componentDidMount() {
+    this.fetchUsers();
+  }
+
+  async fetchUsers() {
+    return await this.props.loadUsers();
+  }
+  render() {
+    const { navModel, searchQuery, setUsersSearchQuery, users } = this.props;
+
+    const linkButton = {
+      href: '/org/users/add',
+      title: 'Add user',
+    };
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">
+          <OrgActionBar
+            searchQuery={searchQuery}
+            showLayoutMode={false}
+            setSearchQuery={setUsersSearchQuery}
+            linkButton={linkButton}
+          />
+          <UsersTable users={users} />
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    navModel: getNavModel(state.navIndex, 'users'),
+    users: getUsers(state.users),
+    searchQuery: getUsersSearchQuery(state.users),
+  };
+}
+
+const mapDispatchToProps = {
+  loadUsers,
+  setUsersSearchQuery,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UsersListPage));

+ 67 - 0
public/app/features/users/UsersTable.tsx

@@ -0,0 +1,67 @@
+import React, { SFC } from 'react';
+import { User } from 'app/types';
+
+export interface Props {
+  users: User[];
+  onRoleChange: (value: string) => {};
+}
+
+const UsersTable: SFC<Props> = props => {
+  const { users } = props;
+
+  return (
+    <div>
+      Le Table
+      <table className="filter-table form-inline">
+        <thead>
+          <tr>
+            <th />
+            <th>Login</th>
+            <th>Email</th>
+            <th>Seen</th>
+            <th>Role</th>
+            <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>
+          );
+        })}
+      </table>
+    </div>
+  );
+};
+
+export default UsersTable;

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

@@ -0,0 +1,40 @@
+import { ThunkAction } from 'redux-thunk';
+import { StoreState } from '../../../types';
+import { getBackendSrv } from '../../../core/services/backend_srv';
+import { User } from 'app/types';
+
+export enum ActionTypes {
+  LoadUsers = 'LOAD_USERS',
+  SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY',
+}
+
+export interface LoadUsersAction {
+  type: ActionTypes.LoadUsers;
+  payload: User[];
+}
+
+export interface SetUsersSearchQueryAction {
+  type: ActionTypes.SetUsersSearchQuery;
+  payload: string;
+}
+
+const usersLoaded = (users: User[]): LoadUsersAction => ({
+  type: ActionTypes.LoadUsers,
+  payload: users,
+});
+
+export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({
+  type: ActionTypes.SetUsersSearchQuery,
+  payload: query,
+});
+
+export type Action = LoadUsersAction | SetUsersSearchQueryAction;
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
+
+export function loadUsers(): ThunkResult<void> {
+  return async dispatch => {
+    const users = await getBackendSrv().get('/api/org/users');
+    dispatch(usersLoaded(users));
+  };
+}

+ 20 - 0
public/app/features/users/state/reducers.ts

@@ -0,0 +1,20 @@
+import { User, UsersState } from 'app/types';
+import { Action, ActionTypes } from './actions';
+
+export const initialState: UsersState = { users: [] as User[], searchQuery: '' };
+
+export const usersReducer = (state = initialState, action: Action): UsersState => {
+  switch (action.type) {
+    case ActionTypes.LoadUsers:
+      return { ...state, users: action.payload };
+
+    case ActionTypes.SetUsersSearchQuery:
+      return { ...state, searchQuery: action.payload };
+  }
+
+  return state;
+};
+
+export default {
+  users: usersReducer,
+};

+ 2 - 0
public/app/features/users/state/selectors.ts

@@ -0,0 +1,2 @@
+export const getUsers = state => state.users;
+export const getUsersSearchQuery = state => state.searchQuery;

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

@@ -9,6 +9,7 @@ import PluginListPage from 'app/features/plugins/PluginListPage';
 import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
 import FolderPermissions from 'app/features/folders/FolderPermissions';
 import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
+import UsersListPage from 'app/features/users/UsersListPage';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -131,9 +132,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'NewOrgCtrl',
     })
     .when('/org/users', {
-      templateUrl: 'public/app/features/org/partials/orgUsers.html',
-      controller: 'OrgUsersCtrl',
-      controllerAs: 'ctrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => UsersListPage,
+      },
     })
     .when('/org/users/invite', {
       templateUrl: 'public/app/features/org/partials/invite.html',

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

@@ -8,6 +8,7 @@ import foldersReducers from 'app/features/folders/state/reducers';
 import dashboardReducers from 'app/features/dashboard/state/reducers';
 import pluginReducers from 'app/features/plugins/state/reducers';
 import dataSourcesReducers from 'app/features/datasources/state/reducers';
+import usersReducers from 'app/features/users/state/reducers';
 
 const rootReducer = combineReducers({
   ...sharedReducers,
@@ -17,6 +18,7 @@ const rootReducer = combineReducers({
   ...dashboardReducers,
   ...pluginReducers,
   ...dataSourcesReducers,
+  ...usersReducers,
 });
 
 export let store;

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

@@ -7,6 +7,7 @@ import { DashboardState } from './dashboard';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { DataSource, DataSourcesState } from './datasources';
 import { PluginMeta, Plugin, PluginsState } from './plugins';
+import { User, UsersState } from './users';
 
 export {
   Team,
@@ -36,6 +37,8 @@ export {
   Plugin,
   PluginsState,
   DataSourcesState,
+  User,
+  UsersState,
 };
 
 export interface StoreState {
@@ -46,4 +49,6 @@ export interface StoreState {
   team: TeamState;
   folder: FolderState;
   dashboard: DashboardState;
+  dataSources: DataSourcesState;
+  users: UsersState;
 }

+ 15 - 0
public/app/types/users.ts

@@ -0,0 +1,15 @@
+export interface User {
+  avatarUrl: string;
+  email: string;
+  lastSeenAt: string;
+  lastSeenAtAge: string;
+  login: string;
+  orgId: number;
+  role: string;
+  userId: number;
+}
+
+export interface UsersState {
+  users: User[];
+  searchQuery: string;
+}