HM-125. Добавление работы с пользователями (#59)

This commit is contained in:
Nikolay
2020-08-30 10:58:19 +03:00
committed by GitHub
parent bfe5394e1d
commit ab0fa2cfc3
13 changed files with 453 additions and 42 deletions

View File

@ -31,7 +31,7 @@ class UsersService {
return data; return data;
} }
getMe = async () => { getSelfInfo = async () => {
const {data} = await http.get(`${ROOT_URL}/me`); const {data} = await http.get(`${ROOT_URL}/me`);
return data; return data;
} }
@ -69,8 +69,8 @@ class UsersService {
* @param {string} login - Логин пользователя * @param {string} login - Логин пользователя
* @param {UpdateUserOptions} updateOptions - настройки для обновления пользователя * @param {UpdateUserOptions} updateOptions - настройки для обновления пользователя
*/ */
update = async (login, updateOptions) => { update = async (user) => {
const {data} = await http.put(ROOT_URL, {...updateOptions, login}); const {data} = await http.put(ROOT_URL, user);
return data; return data;
} }

View File

@ -8,7 +8,7 @@ html, body {
height: 100%; height: 100%;
} }
.Page > div:not(.Login__page) { .Page > div:first-child:not(.Login__page) {
padding-top: 74px; padding-top: 74px;
} }

View File

@ -390,6 +390,21 @@
</div> </div>
</template> </template>
<!-- Шаблоны форм для просмотры логов -->
<template id="user-view-form">
<div class="h-100 overflow-auto d-flex flex-column">
<p class="h2 mb-2 p-3 pr-5 sticky-top border-bottom bg-light"></p>
<form class="p-3 h" autocomplete="off"></form>
<div class="mt-auto p-3 pr-5 border-top bg-light">
<button type="button" class="btn btn-primary UserViewForm__create">Создать</button>
<button type="button" class="btn btn-primary UserViewForm__save">Сохранить</button>
<button type="button" class="btn btn-warning UserViewForm__edit">Изменить</button>
<button type="button" class="btn btn-danger UserViewForm__delete">Удалить</button>
<button type="button" class="btn btn-secondary UserViewForm__cancel">Отмена</button>
</div>
</div>
</template>
<!-- Шаблоны форм для просмотры api --> <!-- Шаблоны форм для просмотры api -->
<template id="api-view-form"> <template id="api-view-form">
<div class="h-100 overflow-auto Api__view-container"> <div class="h-100 overflow-auto Api__view-container">

View File

@ -53,7 +53,7 @@ class AvatarModal extends Modal {
} }
init = async () => { init = async () => {
const user = await usersServiceApi.getMe(); const user = await usersServiceApi.getSelfInfo();
this.changeAvatar(this.url || user.avatar); this.changeAvatar(this.url || user.avatar);
} }

View File

@ -3,6 +3,7 @@ import FormControl from '../form-control';
import {FORM_TYPES} from '../../consts'; import {FORM_TYPES} from '../../consts';
import ModalSidebar from '../modal-sidebar'; import ModalSidebar from '../modal-sidebar';
import './ClientLogsViewForm.css'; import './ClientLogsViewForm.css';
import {INPUT_IDS, LABELS, CLASSNAMES} from './consts';
class ClientLogsViewForm extends Component { class ClientLogsViewForm extends Component {
constructor (parentNode) { constructor (parentNode) {
@ -15,44 +16,50 @@ class ClientLogsViewForm extends Component {
this.title = this.mainNode.querySelector('.h2'); this.title = this.mainNode.querySelector('.h2');
this.form = this.mainNode.querySelector('form'); this.form = this.mainNode.querySelector('form');
this.title.textContent = 'Просмотр клиентского запроса'; this.title.textContent = LABELS.VIEW_CLIENT_REQUEST;
const inputs = [ const inputs = [
this.idInput = this.createComponent(FormControl, this.form, { this.idInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-id', id: INPUT_IDS.ID,
label: 'id', label: LABELS.ID,
}), }),
this.resultInput = this.createComponent(FormControl, this.form, { this.resultInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-result', id: INPUT_IDS.RESULT,
label: 'Результат', label: LABELS.RESULT,
}), }),
this.startTimeInput = this.createComponent(FormControl, this.form, { this.startTimeInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-startTime', id: INPUT_IDS.START_TIME,
label: 'Время запроса', label: LABELS.TIME_REQUEST,
}), }),
this.headersInput = this.createComponent(FormControl, this.form, { this.headersInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-headers', id: INPUT_IDS.HEADERS,
label: 'Заголовки запроса', label: LABELS.HEADERS_REQUEST,
type: FORM_TYPES.TEXTAREA, type: FORM_TYPES.TEXTAREA,
className: 'ClientLogsViewForm__headersInput', className: CLASSNAMES.HEADERS_INPUT,
}),
this.bodyRequest = this.createComponent(FormControl, this.form, {
id: INPUT_IDS.BODY,
label: LABELS.BODY_REQUEST,
type: FORM_TYPES.TEXTAREA,
className: CLASSNAMES.HEADERS_INPUT,
}), }),
this.urlInput = this.createComponent(FormControl, this.form, { this.urlInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-url', id: INPUT_IDS.URL,
label: 'URL запроса', label: LABELS.URL_REQUEST,
}), }),
this.methodInput = this.createComponent(FormControl, this.form, { this.methodInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-method', id: INPUT_IDS.METHOD,
label: 'Метод запроса', label: LABELS.METHOD_REQUEST,
}), }),
this.endTimeInput = this.createComponent(FormControl, this.form, { this.endTimeInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-endTime', id: INPUT_IDS.END_TIME,
label: 'Время ответа', label: LABELS.TIME_RESPONSE,
}), }),
this.responseInput = this.createComponent(FormControl, this.form, { this.responseInput = this.createComponent(FormControl, this.form, {
id: 'client-logs-view-form-response', id: INPUT_IDS.RESPONSE,
label: 'Ответ сервера', label: LABELS.SERVER_RESPONSE,
type: FORM_TYPES.TEXTAREA, type: FORM_TYPES.TEXTAREA,
className: 'ClientLogsViewForm__responseInput', className: CLASSNAMES.RESPONSE_INPUT,
}), }),
]; ];
inputs.forEach((input) => { inputs.forEach((input) => {
@ -65,8 +72,13 @@ class ClientLogsViewForm extends Component {
return JSON.stringify(object, false, 4); return JSON.stringify(object, false, 4);
} }
setForm ({_id, type, request, response, startTime, endTime}) { setRequestBody = (body) => {
const {headers, url, method} = JSON.parse(request) ?? {}; this.bodyRequest.setValue(this.prepareStringJSON(body));
this.bodyRequest.mainNode.style.display = body ? 'block' : 'none';
}
setForm = ({_id, type, request, response, startTime, endTime}) => {
const {headers, url, method, body} = JSON.parse(request) ?? {};
this.idInput.setValue(_id); this.idInput.setValue(_id);
this.resultInput.setValue(type); this.resultInput.setValue(type);
this.startTimeInput.setValue(startTime); this.startTimeInput.setValue(startTime);
@ -75,6 +87,7 @@ class ClientLogsViewForm extends Component {
this.methodInput.setValue(method); this.methodInput.setValue(method);
this.endTimeInput.setValue(endTime); this.endTimeInput.setValue(endTime);
this.responseInput.setValue(this.prepareStringJSON(response)); this.responseInput.setValue(this.prepareStringJSON(response));
this.setRequestBody(body);
this.sidebar.show(); this.sidebar.show();
} }
} }

View File

@ -0,0 +1,29 @@
export const INPUT_IDS = {
ID: 'client-logs-view-form-id',
RESULT: 'client-logs-view-form-result',
START_TIME: 'client-logs-view-form-startTime',
HEADERS: 'client-logs-view-form-headers',
BODY: 'client-logs-view-form-body-request',
URL: 'client-logs-view-form-url',
METHOD: 'client-logs-view-form-method',
END_TIME: 'client-logs-view-form-endTime',
RESPONSE: 'client-logs-view-form-response',
};
export const LABELS = {
ID: 'id',
RESULT: 'Результат',
TIME_REQUEST: 'Время запроса',
HEADERS_REQUEST: 'Заголовки запроса',
BODY_REQUEST: 'Тело запроса',
URL_REQUEST: 'URL запроса',
METHOD_REQUEST: 'Метод запроса',
TIME_RESPONSE: 'Время ответа',
SERVER_RESPONSE: 'Ответ сервера',
VIEW_CLIENT_REQUEST: 'Просмотр клиентского запроса',
};
export const CLASSNAMES = {
HEADERS_INPUT: 'ClientLogsViewForm__headersInput',
RESPONSE_INPUT: 'ClientLogsViewForm__responseInput',
};

View File

@ -1,5 +1,5 @@
import Component from '../component'; import Component from '../component';
import {FORM_TYPES} from '../../consts'; import {FORM_TYPES, EVENTS, TAG_NAME} from '../../consts';
class FormControl extends Component { class FormControl extends Component {
constructor (parentNode, { constructor (parentNode, {
@ -7,9 +7,11 @@ class FormControl extends Component {
id, id,
type = FORM_TYPES.TEXT, type = FORM_TYPES.TEXT,
placeholder = '', placeholder = '',
pattern,
initValue = '', initValue = '',
className = '', className = '',
required = false, required = false,
options = [],
} = {}) { } = {}) {
super('#form-control', parentNode); super('#form-control', parentNode);
@ -18,11 +20,12 @@ class FormControl extends Component {
this.input = this.createElement({ this.input = this.createElement({
tagName: this.getInputTagName(type), tagName: this.getInputTagName(type),
options: { options: {
className: `form-control ${className}`, className: `${this.getInputBaseClassName(type)} ${className}`,
}, },
args: { args: {
type: type === FORM_TYPES.PASSWORD ? 'password' : 'text', type: type === FORM_TYPES.PASSWORD ? 'password' : 'text',
...(required ? {required: ''} : {}), ...(required ? {required: ''} : {}),
...(pattern ? {pattern} : {}),
} }
}); });
this.input.placeholder = placeholder; this.input.placeholder = placeholder;
@ -33,12 +36,31 @@ class FormControl extends Component {
this.label.textContent = label; this.label.textContent = label;
this.label.setAttribute('for', id); this.label.setAttribute('for', id);
this.addEventListener(this.input, 'focus', this.clearError); this.addEventListener(this.input, EVENTS.FOCUS, this.clearError);
this.addEventListener(this.input, 'click', this.clearError); this.addEventListener(this.input, EVENTS.CLICK, this.clearError);
this.addEventListener(this.input, 'keydown', this.clearError); this.addEventListener(this.input, EVENTS.KEYDOWN, this.clearError);
this.addEventListener(this.input, 'input', (evt) => { this.addEventListener(this.input, EVENTS.INPUT, (evt) => {
this.next('input', evt); this.next(EVENTS.INPUT, evt);
}); });
if (type === FORM_TYPES.SELECT) {
options.forEach(({value, text}) => {
this.createElement({
tagName: TAG_NAME.OPTION,
parentNode: this.input,
options: {
textContent: text,
},
args: {
value,
},
});
});
this.addEventListener(this.input, EVENTS.CHANGE, (evt) => {
this.next(EVENTS.CHANGE, evt);
});
}
} }
disabled = (value) => { disabled = (value) => {
@ -61,20 +83,29 @@ class FormControl extends Component {
this.errorText.textContent = ''; this.errorText.textContent = '';
} }
getInputTagName (type) { getInputTagName = (type) => {
switch (type) { switch (type) {
case FORM_TYPES.TEXT: case FORM_TYPES.TEXT:
case FORM_TYPES.PASSWORD: case FORM_TYPES.PASSWORD:
return 'input'; return TAG_NAME.INPUT;
case FORM_TYPES.SELECT: case FORM_TYPES.SELECT:
return 'select'; return TAG_NAME.SELECT;
case FORM_TYPES.TEXTAREA: case FORM_TYPES.TEXTAREA:
return 'textarea'; return TAG_NAME.TEXTAREA;
default: { default: {
throw new Error(`Тип формы ${type} не поддерживается`); throw new Error(`Тип формы ${type} не поддерживается`);
} }
} }
} }
getInputBaseClassName = (type) => {
switch (type) {
case FORM_TYPES.SELECT:
return 'form-select';
default:
return 'form-control';
}
}
} }
export default FormControl; export default FormControl;

View File

@ -23,7 +23,7 @@ class ProfilePage extends Component {
} }
init = async () => { init = async () => {
this.user = await usersServiceApi.getMe(); this.user = await usersServiceApi.getSelfInfo();
this.form.initProfile(this.user); this.form.initProfile(this.user);
} }
} }

View File

@ -0,0 +1,236 @@
import Component from '../component';
import ModalSidebar from '../modal-sidebar';
import FormControl from '../form-control';
import {FORM_TYPES, EVENTS, MODES} from '../../consts';
import routeService from '../../services/RouteService';
import usersServiceApi from '../../api/UsersServiceAPI';
import userInfoService from '../../services/UserInfoService';
const TITLE_MODES = {
[MODES.Create]: 'Создание пользователя',
[MODES.View]: 'Просмотр пользователя',
[MODES.Edit]: 'Редактирование пользователя',
};
const EDIT_MODES = [MODES.Create, MODES.Edit];
const TRUE = 'true';
class UserViewForm extends Component {
constructor (parentNode) {
super('#user-view-form', parentNode);
this.sidebar = this.createComponent(ModalSidebar, {
content: this.mainNode,
});
this.title = this.mainNode.querySelector('.h2');
this.form = this.mainNode.querySelector('form');
this.inputs = [
this.login = this.createComponent(FormControl, this.form, {
id: 'user-view-form-login',
label: 'Логин пользователя',
pattern: '^[a-zA-Z][a-zA-Z0-9_-]{3,}$',
required: true,
offComplete: true,
}),
this.password = this.createComponent(FormControl, this.form, {
id: 'user-view-form-password',
label: 'Пароль',
type: FORM_TYPES.PASSWORD,
}),
this.avatar = this.createComponent(FormControl, this.form, {
id: 'user-view-form-avatar',
label: 'Аватар',
}),
this.isAdmin = this.createComponent(FormControl, this.form, {
id: 'user-view-form-is-admin',
label: 'Админ',
type: FORM_TYPES.SELECT,
options: [
{value: true, text: 'Да'},
{value: false, text: 'Нет'},
]
}),
];
this.password.input.setAttribute('autocomplete', 'new-password');
this.buttons = [
{
button: this.createButton = this.mainNode.querySelector('.UserViewForm__create'),
modes: [MODES.Create],
onlyAdmin: true,
},
{
button: this.saveButton = this.mainNode.querySelector('.UserViewForm__save'),
modes: [MODES.Edit],
onlyAdmin: true,
},
{
button: this.editButton = this.mainNode.querySelector('.UserViewForm__edit'),
modes: [MODES.View],
onlyAdmin: true,
},
{
button: this.cancelButton = this.mainNode.querySelector('.UserViewForm__cancel'),
modes: [MODES.Create, MODES.Edit, MODES.View],
},
{
button: this.deleteButton = this.mainNode.querySelector('.UserViewForm__delete'),
modes: [MODES.View, MODES.Edit],
onlyAdmin: true,
}
];
this.addEventListener(this.cancelButton, 'click', () => {
routeService.pushQuery({}, true);
});
this.addEventListener(this.editButton, 'click', () => {
routeService.pushQuery({mode: MODES.Edit});
});
this.addEventListener(this.form, 'submit', (event) => {
event.preventDefault();
});
this.addEventListener(this.createButton, 'click', this.createUser);
this.addEventListener(this.saveButton, 'click', this.saveUser);
this.addEventListener(this.deleteButton, 'click', this.deleteUser);
this.addSubscribe(routeService, EVENTS.ROUTE_SEARCH_CHANGE, ({query}) => {
const {mode, login} = query;
this.setForm(login, mode);
});
const {query: {mode, login}} = routeService.getUrlData();
if (mode) {
this.setForm(login, mode);
}
this.addSubscribe(userInfoService, EVENTS.CHANGE_USER_INFO, ({is_admin}) => {
this.createButton.disabled = !is_admin;
this.createButton.saveButton = !is_admin;
});
}
setSidebarTitle = (mode, userName) => {
this.title.textContent = [TITLE_MODES[mode], userName].filter(Boolean).join(' ');
}
setInputDisabled = (mode) => {
const disabled = !EDIT_MODES.includes(mode);
this.inputs.forEach((input) => {
const disabledLogin = this.login === input && mode === MODES.Edit;
input.disabled(disabled ? disabled : disabledLogin);
});
}
validateInputs = () => {
this.form.classList.add('was-validated');
const login = this.login.getValue();
const loginErrorMessage = (() => {
if (!login) {
return 'Заполните логин';
}
if (login.length < 4) {
return 'Длинна логина минимум 4 символа';
}
if (!/^[a-z][a-z0-9_-]*$/.test(login)) {
return 'Логин может содержать только латинские буквы, цифры, тире и нижнее подчеркивание';
}
return '';
})();
this.login.setError(loginErrorMessage);
return this.form.checkValidity();
}
createUser = () => {
if (this.validateInputs()) {
this.next(EVENTS.CREATE_USER, {
login: this.login.getValue(),
avatar: this.avatar.getValue(),
password: this.password.getValue(),
is_admin: this.isAdmin.getValue() === TRUE,
});
routeService.pushQuery({}, true);
}
}
saveUser = () => {
if (this.validateInputs()) {
this.next(EVENTS.SAVE_USER, {
login: this.login.getValue(),
avatar: this.avatar.getValue(),
is_admin: this.isAdmin.getValue() === TRUE,
});
routeService.pushQuery({}, true);
}
}
deleteUser = () => {
const {query: {login}} = routeService.getUrlData();
routeService.pushQuery({}, true);
this.next(EVENTS.DELETE_USER, login);
}
loadUser = async (login) => {
if (login) {
return await usersServiceApi.find(login);
}
return {
login: '',
avatar: '',
is_admin: '',
};
}
showButtons = async (mode) => {
const user = await userInfoService.getUserInfo();
this.buttons.forEach(({button, modes, onlyAdmin}) => {
const isShow = modes.includes(mode) && (onlyAdmin ? user.is_admin : true);
button.style.display = isShow ? 'inline-block' : 'none';
});
}
setPassword = (mode) => {
const isShow = mode === MODES.Create;
this.password.setValue('');
this.password.mainNode.style.display = isShow ? 'block' : 'none';
}
setLogin = (mode, login) => {
const disabled = mode !== MODES.Create;
this.login.disabled(disabled);
this.login.setValue(login);
}
setForm = async (userLogin, mode) => {
const {login, avatar, is_admin} = await this.loadUser(userLogin);
this.setSidebarTitle(mode, login);
this.setLogin(mode, login);
this.avatar.setValue(avatar);
this.isAdmin.setValue(!!is_admin);
this.setInputDisabled(mode);
this.showButtons(mode);
this.setPassword(mode);
if (mode) {
this.sidebar.show();
} else {
this.sidebar.hide();
}
}
}
export default UserViewForm;

View File

@ -0,0 +1,3 @@
import UserViewForm from './UserViewForm';
export default UserViewForm;

View File

@ -1,16 +1,69 @@
import Component from '../component'; import Component from '../component';
import UsersTable from '../users-table'; import UsersTable from '../users-table';
import usersServiceApi from '../../api/UsersServiceAPI'; import usersServiceApi from '../../api/UsersServiceAPI';
import UserViewForm from '../user-view-form';
import {EVENTS, MODES} from '../../consts';
import routeService from '../../services/RouteService';
import userInfoService from '../../services/UserInfoService';
class UsersPage extends Component { class UsersPage extends Component {
constructor (mainNodeSelector, parentNode) { constructor (mainNodeSelector, parentNode) {
super(mainNodeSelector, parentNode); super(mainNodeSelector, parentNode);
this.usersForm = this.createComponent(UserViewForm);
this.createElement({tagName: 'div', parentNode: this.mainNode});
this.createUserButton = this.createElement({
tagName: 'button',
parentNode: this.mainNode,
options: {
className: 'btn btn-primary m-3',
textContent: 'Создать пользователя',
},
args: {
type: 'button',
},
});
this.addSubscribe(userInfoService, EVENTS.CHANGE_USER_INFO, ({is_admin}) => {
this.createUserButton.disabled = !is_admin;
});
this.addEventListener(this.createUserButton, 'click', () => {
routeService.pushQuery({mode: MODES.Create}, true);
});
this.addSubscribe(this.usersForm, EVENTS.CREATE_USER, async (user) => {
await usersServiceApi.create(user);
this.initPage();
});
this.addSubscribe(this.usersForm, EVENTS.SAVE_USER, async (user) => {
await usersServiceApi.update(user);
this.initPage();
});
this.addSubscribe(this.usersForm, EVENTS.DELETE_USER, async (login) => {
await usersServiceApi.remove(login);
this.initPage();
});
this.usersTable = this.createComponent(UsersTable, this.mainNode); this.usersTable = this.createComponent(UsersTable, this.mainNode);
this.addSubscribe(this.usersTable, EVENTS.ROW_DOUBLE_CLICK, (_, row) => {
routeService.pushQuery({
mode: MODES.View,
login: row.login,
});
});
this.initPage(); this.initPage();
} }
initPage = async () => { initPage = async () => {
const user = await userInfoService.getUserInfo();
this.createUserButton.disabled = !user.is_admin;
this.userList = await usersServiceApi.request(); this.userList = await usersServiceApi.request();
this.renderTable(); this.renderTable();
} }

View File

@ -55,7 +55,14 @@ export const EVENTS = {
CHANGE_USER_AVATAR: 'changeUserAvatar', CHANGE_USER_AVATAR: 'changeUserAvatar',
OPEN_MODAL: 'openModal', OPEN_MODAL: 'openModal',
CLICK: 'click', CLICK: 'click',
SUBMIT: 'submit' SUBMIT: 'submit',
FOCUS: 'focus',
KEYDOWN: 'keydown',
INPUT: 'input',
CHANGE: 'change',
CREATE_USER: 'createUser',
SAVE_USER: 'saveUser',
DELETE_USER: 'deleteUser',
}; };
export const FORM_TYPES = { export const FORM_TYPES = {
@ -64,3 +71,17 @@ export const FORM_TYPES = {
TEXTAREA: 'TEXTAREA', TEXTAREA: 'TEXTAREA',
PASSWORD: 'PASSWORD', PASSWORD: 'PASSWORD',
}; };
export const MODES = {
Create: 'create',
View: 'view',
Edit: 'edit',
};
export const TAG_NAME = {
OPTION: 'option',
DIV: 'div',
INPUT: 'input',
SELECT: 'select',
TEXTAREA: 'textarea',
};

View File

@ -2,21 +2,31 @@ import usersServiceApi from '../api/UsersServiceAPI';
import {EVENTS} from '../consts'; import {EVENTS} from '../consts';
import EmitService from './EmitService'; import EmitService from './EmitService';
const NOT_USER = 'not_user';
const DEFAULT_AVATAR = 'https://d5qmjlya0ygtg.cloudfront.net/569/c5295/f9ad/47c8/96a0/66a65609b38d/original/331698.jpg';
class UserInfoService extends EmitService { class UserInfoService extends EmitService {
constructor () { constructor () {
super(); super();
this.userInfo = { this.userInfo = {
login: 'not_user', login: NOT_USER,
avatar: 'https://d5qmjlya0ygtg.cloudfront.net/569/c5295/f9ad/47c8/96a0/66a65609b38d/original/331698.jpg', avatar: DEFAULT_AVATAR,
}; };
} }
setUserLogin = async () => { setUserLogin = async () => {
this.userInfo = await usersServiceApi.getMe(); this.userInfo = await usersServiceApi.getSelfInfo();
this.next(EVENTS.CHANGE_USER_INFO, {...this.userInfo}); this.next(EVENTS.CHANGE_USER_INFO, {...this.userInfo});
} }
getUserInfo = async () => {
if (this.userInfo.login === NOT_USER) {
this.userInfo = await usersServiceApi.getSelfInfo();
}
return {...this.userInfo};
}
changeAllAvatars () { changeAllAvatars () {
this.next(EVENTS.CHANGE_USER_AVATAR); this.next(EVENTS.CHANGE_USER_AVATAR);
} }