change(ui) - users list - form
This commit is contained in:
parent
424b071eaf
commit
c97fe55cda
12 changed files with 275 additions and 20 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import UserList from './components/UserList';
|
||||
import { PageTitle } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -6,8 +6,15 @@ import { useObserver } from 'mobx-react-lite';
|
|||
import UserSearch from './components/UserSearch';
|
||||
|
||||
function UsersView(props) {
|
||||
const { userStore } = useStore();
|
||||
const { userStore, roleStore } = useStore();
|
||||
const userCount = useObserver(() => userStore.list.length);
|
||||
const roles = useObserver(() => roleStore.list);
|
||||
|
||||
useEffect(() => {
|
||||
if (roles.length === 0) {
|
||||
roleStore.fetchRoles();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import { Input, CopyButton, Button, Select } from 'UI'
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
interface Props {
|
||||
isSmtp?: boolean;
|
||||
isEnterprise?: boolean;
|
||||
}
|
||||
function UserForm(props: Props) {
|
||||
const { isSmtp = false, isEnterprise = false } = props;
|
||||
const isSaving = false;
|
||||
const { hideModal } = useModal();
|
||||
const { userStore, roleStore } = useStore();
|
||||
const user: any = useObserver(() => userStore.instance);
|
||||
const roles = useObserver(() => roleStore.list.map(r => ({ label: r.name, value: r.roleId })));
|
||||
console.log('roles', roles)
|
||||
|
||||
const onChangeCheckbox = (e: any) => {
|
||||
user.updateKey('isAdmin', !user.isAdmin);
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
}
|
||||
|
||||
const write = ({ target: { name, value } }) => {
|
||||
user.updateKey(name, value);
|
||||
}
|
||||
return useObserver(() => (
|
||||
<div className="bg-white h-screen p-6" style={{ width: '400px'}}>
|
||||
<div className="">
|
||||
<h1 className="text-2xl mb-4">{`${user.exists() ? 'Update' : 'Invite'} User`}</h1>
|
||||
</div>
|
||||
<form onSubmit={ onSave } >
|
||||
<div className="form-group">
|
||||
<label>{ 'Full Name' }</label>
|
||||
<Input
|
||||
name="name"
|
||||
autoFocus
|
||||
value={ user.name }
|
||||
onChange={ write }
|
||||
className="w-full"
|
||||
id="name-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{ 'Email Address' }</label>
|
||||
<Input
|
||||
disabled={user.exists()}
|
||||
name="email"
|
||||
value={ user.email }
|
||||
onChange={ write }
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{ !isSmtp &&
|
||||
<div className={cn("mb-4 p-2 bg-yellow rounded")}>
|
||||
SMTP is not configured (see <a className="link" href="https://docs.openreplay.com/configuration/configure-smtp" target="_blank">here</a> how to set it up). You can still add new users, but you’d have to manually copy then send them the invitation link.
|
||||
</div>
|
||||
}
|
||||
<div className="form-group">
|
||||
<label className="flex items-start cursor-pointer">
|
||||
<input
|
||||
name="admin"
|
||||
type="checkbox"
|
||||
value={ user.isAdmin }
|
||||
checked={ !!user.isAdmin }
|
||||
onChange={ onChangeCheckbox }
|
||||
disabled={user.superAdmin}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="ml-2 select-none">
|
||||
<span>Admin Privileges</span>
|
||||
<div className="text-sm color-gray-medium -mt-1">{ 'Can manage Projects and team members.' }</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ !isEnterprise && (
|
||||
<div className="form-group">
|
||||
<label htmlFor="role">{ 'Role' }</label>
|
||||
<Select
|
||||
placeholder="Role"
|
||||
selection
|
||||
options={ roles }
|
||||
name="roleId"
|
||||
value={ user.roleId }
|
||||
onChange={ write }
|
||||
className="block"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button
|
||||
onClick={ onSave }
|
||||
disabled={ !user.valid() }
|
||||
loading={ isSaving }
|
||||
primary
|
||||
marginRight
|
||||
>
|
||||
{ user.exists() ? 'Update' : 'Invite' }
|
||||
</Button>
|
||||
<Button
|
||||
data-hidden={ !user.exists() }
|
||||
onClick={ hideModal }
|
||||
outline
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</div>
|
||||
{ !user.isJoined && user.invitationLink &&
|
||||
<CopyButton
|
||||
content={user.invitationLink}
|
||||
className="link"
|
||||
btnText="Copy invite link"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default UserForm;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './UserForm';
|
||||
|
|
@ -4,12 +4,15 @@ import React, { useEffect } from 'react';
|
|||
import UserListItem from '../UserListItem';
|
||||
import { sliceListPerPage, getRE } from 'App/utils';
|
||||
import { Pagination, NoContent, Loader } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserForm from '../UserForm';
|
||||
|
||||
function UserList(props) {
|
||||
const { userStore } = useStore();
|
||||
const loading = useObserver(() => userStore.loading);
|
||||
const users = useObserver(() => userStore.list);
|
||||
const searchQuery = useObserver(() => userStore.searchQuery);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const filterList = (list) => {
|
||||
const filterRE = getRE(searchQuery, 'i');
|
||||
|
|
@ -24,11 +27,18 @@ function UserList(props) {
|
|||
|
||||
useEffect(() => {
|
||||
userStore.fetchUsers();
|
||||
editHandler(null);
|
||||
}, []);
|
||||
|
||||
const editHandler = (user) => {
|
||||
userStore.initUser(user).then(() => {
|
||||
showModal(<UserForm />, { right: true });
|
||||
});
|
||||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<NoContent show={!loading && length === 0}>
|
||||
<NoContent show={!loading && length === 0} animatedIcon="empty-state">
|
||||
<div className="mt-3 rounded bg-white">
|
||||
<div className="grid grid-cols-12 p-3 border-b font-medium">
|
||||
<div className="col-span-5">Name</div>
|
||||
|
|
@ -38,7 +48,7 @@ function UserList(props) {
|
|||
|
||||
{sliceListPerPage(list, userStore.page - 1, userStore.pageSize).map((user: any) => (
|
||||
<div key={user.id} className="">
|
||||
<UserListItem user={user} />
|
||||
<UserListItem user={user} editHandler={() => editHandler(user)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
interface Props {
|
||||
user: any;
|
||||
editHandler?: any;
|
||||
}
|
||||
function UserListItem(props: Props) {
|
||||
const { user } = props;
|
||||
const { user, editHandler = () => {} } = props;
|
||||
return (
|
||||
<div className="grid grid-cols-12 p-3 py-6 border-b select-none hover:bg-active-blue">
|
||||
<div className="col-span-5">{user.email}</div>
|
||||
<div className="grid grid-cols-12 p-3 py-4 border-b items-center select-none hover:bg-active-blue group">
|
||||
<div className="col-span-5">{user.name}</div>
|
||||
<div className="col-span-3">
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border color-teal text-sm capitalize">
|
||||
{user.roleName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
|
||||
<div className="col-span-4 justify-self-end invisible group-hover:visible">
|
||||
<button className='' onClick={editHandler}>
|
||||
<Icon name="pencil" color="teal" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import DashboardStore, { IDashboardSotre } from './dashboardStore';
|
||||
import MetricStore, { IMetricStore } from './metricStore';
|
||||
import UserStore from './userStore';
|
||||
import RoleStore from './roleStore';
|
||||
import APIClient from 'App/api_client';
|
||||
import { dashboardService, metricService, sessionService, userService } from 'App/services';
|
||||
import SettingsStore from './settingsStore';
|
||||
|
|
@ -11,12 +12,14 @@ export class RootStore {
|
|||
metricStore: IMetricStore;
|
||||
settingsStore: SettingsStore;
|
||||
userStore: UserStore;
|
||||
roleStore: RoleStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
this.metricStore = new MetricStore();
|
||||
this.settingsStore = new SettingsStore();
|
||||
this.userStore = new UserStore();
|
||||
this.roleStore = new RoleStore();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
|
|
|
|||
31
frontend/app/mstore/roleStore.ts
Normal file
31
frontend/app/mstore/roleStore.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { makeAutoObservable, observable, action } from "mobx"
|
||||
import { userService } from "App/services";
|
||||
import Role, { IRole } from "./types/role";
|
||||
|
||||
export default class UserStore {
|
||||
list: IRole[] = [];
|
||||
loading: boolean = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
list: observable,
|
||||
loading: observable,
|
||||
})
|
||||
}
|
||||
|
||||
fetchRoles(): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.getRoles()
|
||||
.then(response => {
|
||||
this.list = response.map((role: any) => new Role().fromJson(role));
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
42
frontend/app/mstore/types/role.ts
Normal file
42
frontend/app/mstore/types/role.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { makeAutoObservable, observable, runInAction } from "mobx";
|
||||
|
||||
export interface IRole {
|
||||
roleId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
fromJson(json: any): IRole;
|
||||
toJson(): any;
|
||||
}
|
||||
|
||||
export default class Role implements IRole {
|
||||
roleId: string = '';
|
||||
name: string = '';
|
||||
description: string = '';
|
||||
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
roleId: observable,
|
||||
name: observable,
|
||||
description: observable,
|
||||
})
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
this.roleId = json.id;
|
||||
this.name = json.name;
|
||||
this.description = json.description;
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
id: this.roleId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { runInAction, makeAutoObservable, observable } from 'mobx'
|
||||
import { DateTime } from 'luxon';
|
||||
import { validateEmail } from 'App/validate';
|
||||
|
||||
export interface IUser {
|
||||
userId: string
|
||||
email: string
|
||||
|
|
@ -16,8 +18,9 @@ export interface IUser {
|
|||
toJson(): any
|
||||
}
|
||||
|
||||
export default class User {
|
||||
export default class User implements IUser {
|
||||
userId: string = '';
|
||||
name: string = '';
|
||||
email: string = '';
|
||||
createdAt: string = '';
|
||||
isAdmin: boolean = false;
|
||||
|
|
@ -43,9 +46,17 @@ export default class User {
|
|||
})
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
console.log(key, value)
|
||||
runInAction(() => {
|
||||
this[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
this.userId = json.id;
|
||||
this.name = json.name;
|
||||
this.email = json.email;
|
||||
this.createdAt = json.createdAt && DateTime.fromISO(json.createdAt || 0)
|
||||
this.isAdmin = json.admin
|
||||
|
|
@ -67,4 +78,12 @@ export default class User {
|
|||
roleId: this.roleId,
|
||||
}
|
||||
}
|
||||
|
||||
valid() {
|
||||
return validateEmail(this.email) && !!this.roleId;
|
||||
}
|
||||
|
||||
exists() {
|
||||
return !!this.userId;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,18 @@ export default class UserStore {
|
|||
instance: observable,
|
||||
updateUser: action,
|
||||
updateKey: action,
|
||||
initUser: action,
|
||||
})
|
||||
}
|
||||
|
||||
initUser(user?: any ): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (user) {
|
||||
this.instance = user;
|
||||
} else {
|
||||
this.instance = new User();
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,4 +42,10 @@ export default class UserService {
|
|||
.then(response => response.json())
|
||||
.then(response => response.data || {});
|
||||
}
|
||||
|
||||
getRoles() {
|
||||
return this.client.get('/client/roles')
|
||||
.then(response => response.json())
|
||||
.then(response => response.data || []);
|
||||
}
|
||||
}
|
||||
|
|
@ -147,13 +147,4 @@
|
|||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* .svg-map__location {
|
||||
fill: #EEE !important;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
fill: #fff !important;
|
||||
}
|
||||
} */
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue