src/visits/VisitsTable.js   A
last analyzed

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 207
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 88.1%

Importance

Changes 0
Metric Value
wmc 8
eloc 179
mnd 8
bc 8
fnc 0
dl 0
loc 207
ccs 37
cts 42
cp 0.881
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import React, { useEffect, useMemo, useState } from 'react';
2
import PropTypes from 'prop-types';
3
import Moment from 'react-moment';
4
import classNames from 'classnames';
5
import { min, splitEvery } from 'ramda';
6
import {
7
  faCaretDown as caretDownIcon,
8
  faCaretUp as caretUpIcon,
9
  faCheck as checkIcon,
10
} from '@fortawesome/free-solid-svg-icons';
11
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
import SimplePaginator from '../common/SimplePaginator';
13
import SearchField from '../utils/SearchField';
14
import { determineOrderDir } from '../utils/utils';
15
import { prettify } from '../utils/helpers/numbers';
16
import './VisitsTable.scss';
17
18 2
const NormalizedVisitType = PropTypes.shape({
19
20
});
21
22 2
const propTypes = {
23
  visits: PropTypes.arrayOf(NormalizedVisitType).isRequired,
24
  selectedVisits: PropTypes.arrayOf(NormalizedVisitType),
25
  setSelectedVisits: PropTypes.func.isRequired,
26
  isSticky: PropTypes.bool,
27
  matchMedia: PropTypes.func,
28
};
29
30 2
const PAGE_SIZE = 20;
31 2
const visitMatchesSearch = ({ browser, os, referer, country, city }, searchTerm) =>
32 18
  `${browser} ${os} ${referer} ${country} ${city}`.toLowerCase().includes(searchTerm.toLowerCase());
33 18
const searchVisits = (searchTerm, visits) => visits.filter((visit) => visitMatchesSearch(visit, searchTerm));
34 3
const sortVisits = ({ field, dir }, visits) => visits.sort((a, b) => {
35 24
  const greaterThan = dir === 'ASC' ? 1 : -1;
36 24
  const smallerThan = dir === 'ASC' ? -1 : 1;
37
38 24
  return a[field] > b[field] ? greaterThan : smallerThan;
39
});
40 2
const calculateVisits = (allVisits, searchTerm, order) => {
41 37
  const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [ ...allVisits ];
42 37
  const sortedVisits = order.dir ? sortVisits(order, filteredVisits) : filteredVisits;
43 37
  const total = sortedVisits.length;
44 37
  const visitsGroups = splitEvery(PAGE_SIZE, sortedVisits);
45
46 37
  return { visitsGroups, total };
47
};
48
49 2
const VisitsTable = ({
50
  visits,
51
  selectedVisits = [],
52
  setSelectedVisits,
53
  isSticky = false,
54
  matchMedia = window.matchMedia,
55
}) => {
56 37
  const headerCellsClass = classNames('visits-table__header-cell', {
57
    'visits-table__sticky': isSticky,
58
  });
59 37
  const matchMobile = () => matchMedia('(max-width: 767px)').matches;
60
61 37
  const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
62 37
  const [ searchTerm, setSearchTerm ] = useState(undefined);
63 37
  const [ order, setOrder ] = useState({ field: undefined, dir: undefined });
64 37
  const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
65
66 37
  const [ page, setPage ] = useState(1);
67 37
  const end = page * PAGE_SIZE;
68 37
  const start = end - PAGE_SIZE;
69
70 222
  const orderByColumn = (field) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
71 222
  const renderOrderIcon = (field) => order.dir && order.field === field && (
72
    <FontAwesomeIcon
73
      icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon}
74
      className="visits-table__header-icon"
75
    />
76
  );
77
78 37
  useEffect(() => {
79
    const listener = () => setIsMobileDevice(matchMobile());
80
81
    window.addEventListener('resize', listener);
82
83
    return () => window.removeEventListener('resize', listener);
84
  }, []);
85 37
  useEffect(() => {
86
    setPage(1);
87
    setSelectedVisits([]);
88
  }, [ searchTerm ]);
89
90 37
  return (
91
    <table className="table table-striped table-bordered table-hover table-sm table-responsive-sm visits-table">
92
      <thead className="visits-table__header">
93
        <tr>
94
          <th
95
            className={classNames('visits-table__header-cell text-center', {
96
              'visits-table__sticky': isSticky,
97
            })}
98 1
            onClick={() => setSelectedVisits(
99
              selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : []
100
            )}
101
          >
102
            <FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
103
          </th>
104
          <th className={headerCellsClass} onClick={orderByColumn('date')}>
105
            Date
106
            {renderOrderIcon('date')}
107
          </th>
108
          <th className={headerCellsClass} onClick={orderByColumn('country')}>
109
            Country
110
            {renderOrderIcon('country')}
111
          </th>
112
          <th className={headerCellsClass} onClick={orderByColumn('city')}>
113
            City
114
            {renderOrderIcon('city')}
115
          </th>
116
          <th className={headerCellsClass} onClick={orderByColumn('browser')}>
117
            Browser
118
            {renderOrderIcon('browser')}
119
          </th>
120
          <th className={headerCellsClass} onClick={orderByColumn('os')}>
121
            OS
122
            {renderOrderIcon('os')}
123
          </th>
124
          <th className={headerCellsClass} onClick={orderByColumn('referer')}>
125
            Referrer
126
            {renderOrderIcon('referer')}
127
          </th>
128
        </tr>
129
        <tr>
130
          <td colSpan={7} className="p-0">
131
            <SearchField noBorder large={false} onChange={setSearchTerm} />
132
          </td>
133
        </tr>
134
      </thead>
135
      <tbody>
136
        {(!resultSet.visitsGroups[page - 1] || resultSet.visitsGroups[page - 1].length === 0) && (
137
          <tr>
138
            <td colSpan={7} className="text-center">
139
              No visits found with current filtering
140
            </td>
141
          </tr>
142
        )}
143
        {resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => {
144 392
          const isSelected = selectedVisits.includes(visit);
145
146 392
          return (
147
            <tr
148
              key={index}
149
              style={{ cursor: 'pointer' }}
150
              className={classNames({ 'table-primary': isSelected })}
151 2
              onClick={() => setSelectedVisits(
152 2
                isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ]
153
              )}
154
            >
155
              <td className="text-center">
156
                {isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
157
              </td>
158
              <td>
159
                <Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
160
              </td>
161
              <td>{visit.country}</td>
162
              <td>{visit.city}</td>
163
              <td>{visit.browser}</td>
164
              <td>{visit.os}</td>
165
              <td>{visit.referer}</td>
166
            </tr>
167
          );
168
        })}
169
      </tbody>
170
      {resultSet.total > PAGE_SIZE && (
171
        <tfoot>
172
          <tr>
173
            <td colSpan={7} className={classNames('visits-table__footer-cell', { 'visits-table__sticky': isSticky })}>
174
              <div className="row">
175
                <div className="col-md-6">
176
                  <SimplePaginator
177
                    pagesCount={Math.ceil(resultSet.total / PAGE_SIZE)}
178
                    currentPage={page}
179
                    setCurrentPage={setPage}
180
                    centered={isMobileDevice}
181
                  />
182
                </div>
183
                <div
184
                  className={classNames('col-md-6', {
185
                    'd-flex align-items-center flex-row-reverse': !isMobileDevice,
186
                    'text-center mt-3': isMobileDevice,
187
                  })}
188
                >
189
                  <div>
190
                    Visits <b>{prettify(start + 1)}</b> to{' '}
191
                    <b>{prettify(min(end, resultSet.total))}</b> of{' '}
192
                    <b>{prettify(resultSet.total)}</b>
193
                  </div>
194
                </div>
195
              </div>
196
            </td>
197
          </tr>
198
        </tfoot>
199
      )}
200
    </table>
201
  );
202
};
203
204 2
VisitsTable.propTypes = propTypes;
205
206
export default VisitsTable;
207