Passed
Push — main ( 017db1...f1f3c3 )
by Alejandro
42:56 queued 39:55
created

src/visits/VisitsStats.tsx   A

Complexity

Total Complexity 14
Complexity/F 0

Size

Lines of Code 260
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 74.51%

Importance

Changes 0
Metric Value
wmc 14
eloc 221
mnd 14
bc 14
fnc 0
dl 0
loc 260
ccs 38
cts 51
cp 0.7451
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import { isEmpty, propEq, values } from 'ramda';
2
import { useState, useEffect, useMemo, FC } from 'react';
3
import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
4
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
6
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
7
import moment from 'moment';
8
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
9
import Message from '../utils/Message';
10
import { formatDate } from '../utils/helpers/date';
11
import { ShlinkVisitsParams } from '../utils/services/types';
12
import SortableBarGraph from './helpers/SortableBarGraph';
13
import GraphCard from './helpers/GraphCard';
14
import LineChartCard from './helpers/LineChartCard';
15
import VisitsTable from './VisitsTable';
16
import { NormalizedVisit, Stats, VisitsInfo } from './types';
17
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
18
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
19
import './VisitsStats.scss';
20
21
export interface VisitsStatsProps {
22
  getVisits: (params: Partial<ShlinkVisitsParams>) => void;
23
  visitsInfo: VisitsInfo;
24
  cancelGetVisits: () => void;
25
}
26
27
type HighlightableProps = 'referer' | 'country' | 'city';
28
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
29
30 3
const sections: Record<Section, { title: string; icon: IconDefinition }> = {
31
  byTime: { title: 'By time', icon: faCalendarAlt },
32
  byContext: { title: 'By context', icon: faChartPie },
33
  byLocation: { title: 'By location', icon: faMapMarkedAlt },
34
  list: { title: 'List', icon: faList },
35
};
36
37 3
const highlightedVisitsToStats = (
38
  highlightedVisits: NormalizedVisit[],
39
  prop: HighlightableProps,
40 5
): Stats => highlightedVisits.reduce<Stats>((acc, highlightedVisit) => {
41 2
  if (!acc[highlightedVisit[prop]]) {
42
    acc[highlightedVisit[prop]] = 0;
43
  }
44
45
  acc[highlightedVisit[prop]] += 1;
46
47
  return acc;
48
}, {});
49 3
const format = formatDate();
50
let selectedBar: string | undefined;
51
52 3
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
53 14
  const [ startDate, setStartDate ] = useState<moment.Moment | null>(null);
54 14
  const [ endDate, setEndDate ] = useState<moment.Moment | null>(null);
55 14
  const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
56 14
  const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
57 14
  const [ activeSection, setActiveSection ] = useState<Section>('byTime');
58 40
  const onSectionChange = (section: Section) => () => setActiveSection(section);
59
60 14
  const { visits, loading, loadingLarge, error, progress } = visitsInfo;
61 14
  const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
62 14
  const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
63 9
    () => processStatsFromVisits(normalizedVisits),
64
    [ normalizedVisits ],
65
  );
66 14
  const mapLocations = values(citiesForMap);
67
68 14
  const setSelectedVisits = (selectedVisits: NormalizedVisit[]) => {
69
    selectedBar = undefined;
70
    setHighlightedVisits(selectedVisits);
71
  };
72 14
  const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => {
73
    const newSelectedBar = `${prop}_${value}`;
74
75 2
    if (selectedBar === newSelectedBar) {
76
      setHighlightedVisits([]);
77
      setHighlightedLabel(undefined);
78
      selectedBar = undefined;
79
    } else {
80
      setHighlightedVisits(normalizedVisits.filter(propEq(prop, value)));
81
      setHighlightedLabel(value);
82
      selectedBar = newSelectedBar;
83
    }
84
  };
85
86 14
  useEffect(() => () => cancelGetVisits(), []);
87 14
  useEffect(() => {
88 4
    getVisits({ startDate: format(startDate) ?? undefined, endDate: format(endDate) ?? undefined });
89
  }, [ startDate, endDate ]);
90
91 14
  const renderVisitsContent = () => {
92 14
    if (loadingLarge) {
93 1
      return (
94
        <Message loading>
95
          This is going to take a while... :S
96
          <Progress value={progress} striped={progress === 100} className="mt-3" />
97
        </Message>
98
      );
99
    }
100
101 13
    if (loading) {
102 1
      return <Message loading />;
103
    }
104
105 12
    if (error) {
106 1
      return (
107
        <Card className="mt-4" body inverse color="danger">
108
          An error occurred while loading visits :(
109
        </Card>
110
      );
111
    }
112
113 11
    if (isEmpty(visits)) {
114 1
      return <Message>There are no visits matching current filter  :(</Message>;
115
    }
116
117 10
    return (
118
      <>
119
        <Card className="visits-stats__nav p-0 mt-4 overflow-hidden" body>
120
          <Nav pills justified>
121
            {Object.entries(sections).map(
122
              ([ section, { title, icon }]) => (
123 40
                <NavLink
124
                  key={section}
125
                  active={activeSection === section}
126
                  className="visits-stats__nav-link"
127
                  onClick={onSectionChange(section as Section)}
128
                >
129
                  <FontAwesomeIcon icon={icon} />
130
                  <span className="ml-2 d-none d-sm-inline">{title}</span>
131
                </NavLink>
132
              ),
133
            )}
134
          </Nav>
135
        </Card>
136
        <div className="row">
137
          {activeSection === 'byTime' && (
138
            <div className="col-12 mt-4">
139
              <LineChartCard
140
                title="Visits during time"
141
                visits={normalizedVisits}
142
                highlightedVisits={highlightedVisits}
143
                highlightedLabel={highlightedLabel}
144
                setSelectedVisits={setSelectedVisits}
145
              />
146
            </div>
147
          )}
148
          {activeSection === 'byContext' && (
149
            <>
150
              <div className="col-xl-4 col-lg-6 mt-4">
151
                <GraphCard title="Operating systems" stats={os} />
152
              </div>
153
              <div className="col-xl-4 col-lg-6 mt-4">
154
                <GraphCard title="Browsers" stats={browsers} />
155
              </div>
156
              <div className="col-xl-4 mt-4">
157
                <SortableBarGraph
158
                  title="Referrers"
159
                  stats={referrers}
160
                  withPagination={false}
161
                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
162
                  highlightedLabel={highlightedLabel}
163
                  sortingItems={{
164
                    name: 'Referrer name',
165
                    amount: 'Visits amount',
166
                  }}
167
                  onClick={highlightVisitsForProp('referer')}
168
                />
169
              </div>
170
            </>
171
          )}
172
          {activeSection === 'byLocation' && (
173
            <>
174
              <div className="col-lg-6 mt-4">
175
                <SortableBarGraph
176
                  title="Countries"
177
                  stats={countries}
178
                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
179
                  highlightedLabel={highlightedLabel}
180
                  sortingItems={{
181
                    name: 'Country name',
182
                    amount: 'Visits amount',
183
                  }}
184
                  onClick={highlightVisitsForProp('country')}
185
                />
186
              </div>
187
              <div className="col-lg-6 mt-4">
188
                <SortableBarGraph
189
                  title="Cities"
190
                  stats={cities}
191
                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
192
                  highlightedLabel={highlightedLabel}
193
                  extraHeaderContent={(activeCities: string[]) =>
194 2
                    mapLocations.length > 0 &&
195
                    <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
196
                  }
197
                  sortingItems={{
198
                    name: 'City name',
199
                    amount: 'Visits amount',
200
                  }}
201
                  onClick={highlightVisitsForProp('city')}
202
                />
203
              </div>
204
            </>
205
          )}
206
          {activeSection === 'list' && (
207
            <div className="col-12">
208
              <VisitsTable
209
                visits={normalizedVisits}
210
                selectedVisits={highlightedVisits}
211
                setSelectedVisits={setSelectedVisits}
212
                isSticky
213
              />
214
            </div>
215
          )}
216
        </div>
217
      </>
218
    );
219
  };
220
221 14
  return (
222
    <>
223
      {children}
224
225
      <section className="mt-4">
226
        <div className="row flex-md-row-reverse">
227
          <div className="col-lg-7 col-xl-6">
228
            <DateRangeSelector
229
              disabled={loading}
230
              defaultText="All visits"
231
              onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
232 2
                setStartDate(newStartDate ?? null);
233 2
                setEndDate(newEndDate ?? null);
234
              }}
235
            />
236
          </div>
237
          {visits.length > 0 && (
238
            <div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
239
              <Button
240
                outline
241
                disabled={highlightedVisits.length === 0}
242
                className="btn-md-block"
243
                onClick={() => setSelectedVisits([])}
244
              >
245
                Reset selection {highlightedVisits.length > 0 && <>({highlightedVisits.length})</>}
246
              </Button>
247
            </div>
248
          )}
249
        </div>
250
      </section>
251
252
      <section>
253
        {renderVisitsContent()}
254
      </section>
255
    </>
256
  );
257
};
258
259
export default VisitsStats;
260