Passed
Push — main ( f1f3c3...623dee )
by Alejandro
39:02 queued 36:46
created

src/visits/VisitsStats.tsx   A

Complexity

Total Complexity 10
Complexity/F 0

Size

Lines of Code 259
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 71.43%

Importance

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