first crude display

This commit is contained in:
Peter Holmberg 2018-09-28 17:21:00 +02:00
parent da856187d8
commit 8f99276606
12 changed files with 234 additions and 11 deletions

View File

@ -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">

View File

@ -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(),
};

View File

@ -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}
/>

View File

@ -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));

View File

@ -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;

View File

@ -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));
};
}

View File

@ -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,
};

View File

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

View File

@ -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',

View File

@ -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;

View File

@ -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
public/app/types/users.ts Normal file
View File

@ -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;
}