HM-56. Добавлена страница для просмотра логов с фильтрацией, пагинацией (#20)

This commit is contained in:
Nikolay
2020-07-19 16:19:39 +03:00
committed by GitHub
parent d01ee79202
commit 47fe69bc65
20 changed files with 520 additions and 10 deletions

View File

@ -43,12 +43,19 @@ class Component extends EmitService {
}
/**
* Метод уничтожения компонента. Удаляет элемент из верстки, снимает обработчики и очищает подписки
* Метод удаляет всех слушателей текущего компонента
*/
destroy = () => {
clearListeners = () => {
this._listeners.forEach(({element, eventName, listener}) => {
element.removeEventListener(eventName, listener);
});
}
/**
* Метод уничтожения компонента. Удаляет элемент из верстки, снимает обработчики и очищает подписки
*/
destroy = () => {
this.clearListeners();
this.mainNode.remove();
this._listeners = [];
this.clearSubscribes();

View File

@ -0,0 +1,25 @@
import Component from '../component/index';
import routeService from '../../services/RouteService';
class LogsFilters extends Component {
constructor (parentNode) {
super('#logs-filters', parentNode);
this.messageInput = this.mainNode.querySelector('#logs-filter-message');
const {message = ''} = routeService.getUrlData().query;
this.messageInput.value = message;
this.addEventListener(this.mainNode, 'submit', (event) => {
event.preventDefault();
routeService.pushQuery({
message: this.messageInput.value,
});
});
}
}
export default LogsFilters;

View File

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

View File

@ -0,0 +1,83 @@
import Component from '../component/index';
import Table from '../table';
import storageLogsApi from '../../api/StorageLogsAPI';
import Pagination from '../pagination';
import LogsFilters from '../logs-filters';
import routeService from '../../services/RouteService';
import {prepareToNumber} from '../../utils/urlUtils';
const COLS = [
{id: '_id', label: 'id', width: '240px'},
{id: 'date', label: 'Дата', width: '150px'},
{id: 'type', label: 'Тип', width: '70px'},
{id: 'message', label: 'Сообщение', width: '200px'},
{id: 'trace', label: 'Стек', width: '200px'},
];
const ELEMENTS_ON_PAGE = 15;
class LogsPage extends Component {
constructor (mainNodeSelector, parentNode) {
super(mainNodeSelector, parentNode);
this.filters = new LogsFilters(this.mainNode);
this.table = new Table(this.mainNode, COLS);
this.pagination = new Pagination(this.mainNode);
routeService.onChange(this.renderTable);
this.pagination.onPageChange((pageNumber) => {
const start = (pageNumber - 1) * ELEMENTS_ON_PAGE;
const end = start + ELEMENTS_ON_PAGE;
const rows = this.logList.slice(start, end + 1);
this.table.render(rows);
});
this.loadLogList();
}
loadLogList = async () => {
this.logList = await storageLogsApi.request();
this.renderTable();
}
filterRows = (queryMessage) => {
return this.logList.reduce((memo, row) => {
const message = row.message.toLowerCase();
const searchMessage = (queryMessage || '').toLowerCase();
if (searchMessage === '') {
memo.push(row);
return memo;
}
if (message.includes(searchMessage)) {
const replaceMessage = new RegExp(searchMessage, 'g');
const newText = message.replace(replaceMessage, `<span class="text-warning bg-dark">${searchMessage}</span>`);
memo.push({
...row,
message: newText,
});
}
return memo;
}, []);
}
renderTable = () => {
const {query} = routeService.getUrlData();
const filterRows = this.filterRows(query.message);
const countPages = Math.ceil(filterRows.length / ELEMENTS_ON_PAGE);
const pageNumber = prepareToNumber(query.pageNumber, countPages);
this.pagination.changeCountPages(countPages);
const start = (pageNumber - 1) * ELEMENTS_ON_PAGE;
const end = start + ELEMENTS_ON_PAGE;
const rows = filterRows.slice(start, end + 1);
this.table.render(rows);
}
}
export default LogsPage;

View File

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

View File

@ -8,7 +8,7 @@ export const NAV_MENU = [
url: '/api'
},
{
title: 'Еще что-то',
url: '/something'
title: 'Журнал',
url: '/logs'
},
];

View File

@ -0,0 +1,111 @@
import Component from '../component/index';
import routeService from '../../services/RouteService';
import {createElement} from '../../utils/elementUtils';
import {prepareToNumber} from '../../utils/urlUtils';
const LEFT_ICON = '&laquo;';
const RIGHT_ICON = '&raquo;';
const CHAGE_PAGE = 'changePage';
class Pagination extends Component {
buttons = [];
constructor (parentNode) {
super('#pagination', parentNode);
this.currentPage = 0;
this.countPages = 0;
this.container = this.mainNode.querySelector('.pagination');
routeService.onChange(() => {
this.renderButtons();
});
}
renderOneButton = (text, isDisabled = false) => {
const li = createElement({
tagName: 'li',
parentNode: this.container,
options: {
className: 'page-item',
disabled: isDisabled,
},
});
const button = createElement({
tagName: 'button',
parentNode: li,
options: {
className: 'page-link pe-auto',
innerHTML: text,
},
});
if (isDisabled) {
li.classList.add('disabled');
button.setAttribute('disabled', 'true');
}
if (text === this.currentPage) {
li.classList.add('active');
}
this.addEventListener(button, 'click', () => {
const nextPage = (() => {
if (text === LEFT_ICON) {
return this.currentPage - 1;
}
if (text === RIGHT_ICON) {
return this.currentPage + 1;
}
return text;
})();
routeService.pushQuery({
pageNumber: nextPage,
});
});
this.buttons.push(li);
}
clearButtons = () => {
this.buttons.forEach((b) => {
b.remove();
});
this.clearListeners();
this.buttons = [];
}
changeCountPages = (countPages) => {
if (countPages !== this.countPages) {
this.countPages = countPages;
this.renderButtons();
}
}
renderButtons = () => {
this.clearButtons();
const {pageNumber} = routeService.getUrlData().query;
this.currentPage = prepareToNumber(pageNumber, this.countPages);
const start = this.currentPage < 3 ? 1 : this.currentPage - 2;
const end = start + 4 > this.countPages ? this.countPages : start + 4;
this.renderOneButton(LEFT_ICON, this.currentPage <= 1);
for (let i = start; i <= end; i += 1) {
this.renderOneButton(i);
}
this.renderOneButton(RIGHT_ICON, this.currentPage >= this.countPages);
this.next(CHAGE_PAGE, this.currentPage);
}
onPageChange = (listener) => {
this.subscribe(CHAGE_PAGE, listener);
}
}
export default Pagination;

View File

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

View File

@ -0,0 +1,13 @@
import Component from '../component/index';
class HeaderCol extends Component {
constructor (parentNode, col) {
super('#uni-table-th', parentNode);
this.id = col.id;
this.mainNode.textContent = col.label;
this.mainNode.style.minWidth = col.width ?? '';
}
}
export default HeaderCol;

View File

@ -0,0 +1,89 @@
import Component from '../component/index';
import HeaderCol from './HeaderCol';
import TableRow from './TableRow';
const ROW_CLICK = 'ROW_CLICK';
/**
* @interface Col
* @type {Object}
* @property {string} id - id колонки, которая соответствует ключу объекта
* @property {string} label - читаемое название колонки
*/
/**
* Класс создания Таблицы. В нее при создании передаем массив с колонками, а сами строчки рендерим в тот момент,
* когда у нас есть на руках сформированные данные
* @param {Node} parentNode
* @param {Col[]} cols
*
* @example
* const table = new Table(document.body, [
* {id: 'key', label: 'Ключ'},
* {id: 'value', label: 'Значение'},
* {id: 'author', label: 'Автор'},
* ]);
*
* table.render([
* {key: 'testApi', value: '', author: 'Tester'},
* {key: 'testApi2', value: '', author: 'Tester'},
* ]);
*/
class Table extends Component {
constructor (parentNode, cols) {
super('#uni-table', parentNode);
this.theadTr = this.mainNode.querySelector('thead tr');
this.tbody = this.mainNode.querySelector('tbody');
this.cols = cols;
this.renderHeader();
}
/**
* Метод нужен для отрисовки шапки таблицы, вызывается конструктором самостоятельно
*/
renderHeader = () => {
if (this.headerCols) {
this.headerCols.forEach((col) => {
col.destroy();
});
}
this.headerCols = this.cols.map((col) => new HeaderCol(this.theadTr, col));
}
/**
* Метод рендерит строчки таблицы
* @param {{[key: string]: string}[]} rows - список строк для рендера
*/
render = (rows) => {
if (this.rows) {
this.rows.forEach((row) => {
row.destroy();
});
}
this.rows = rows.map((row) => {
const rowComponent = new TableRow(this.tbody, this.cols, row);
rowComponent.onClick((event) => {
this.next(ROW_CLICK, event, row);
});
return rowComponent;
});
}
/**
* Метод для подписки на клики по строчкам, передает event и данные строчки
* @param {Function} listener - слушатель
*
* @example
* table.onRowClick((event, row) => {
* // Ваш код
* });
*/
onRowClick = (listener) => {
this.subscribe(ROW_CLICK, listener);
}
}
export default Table;

View File

@ -0,0 +1,31 @@
import Component from '../component/index';
import {createElement} from '../../utils/elementUtils';
const CLICK = 'click';
class TableRow extends Component {
constructor (parentNode, cols, row) {
super('#uni-table-row', parentNode);
cols.forEach((col) => {
const cell = createElement({
tagName: 'td',
parentNode: this.mainNode,
options: {
className: 'text-break'
},
});
cell.innerHTML = row[col.id];
});
this.mainNode.addEventListener(CLICK, (event) => {
this.next(CLICK, event);
});
}
onClick = (listener) => {
this.subscribe(CLICK, listener);
}
}
export default TableRow;

View File

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