Repository: shlinkio/shlink-web-client
Pull request #347 (2020-12-12 20:08:00)

Feature/visits improvements
Refs #285 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ae2abb0..15318b2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 * [#309](https://github.com/shlinkio/shlink-web-client/issues/309) Added new domain selector component in create URL form which allows selecting from previously used domains or set a new one.
 * [#315](https://github.com/shlinkio/shlink-web-client/issues/315) Now you can tell if you want to validate the long URL when using Shlink >=2.4.
+* [#285](https://github.com/shlinkio/shlink-web-client/issues/285) Improved visits section:
+
+    * Charts are now grouped in tabs, so that only one part of the components is rendered at a time.
+    * Amount of highlighted visits is now displayed.
 
 ### Changed
 * [#267](https://github.com/shlinkio/shlink-web-client/issues/267) Added some subtle but important improvements on UI/UX.
diff --git a/src/common/AsideMenu.scss b/src/common/AsideMenu.scss
index af1935b7..22547a50 100644
--- a/src/common/AsideMenu.scss
+++ b/src/common/AsideMenu.scss
@@ -48,7 +48,7 @@ $asideMenuMobileWidth: 280px;
 }
 
 .aside-menu__item:hover {
-  background-color: $lightHoverColor;
+  background-color: $lightColor;
 }
 
 .aside-menu__item--selected {
diff --git a/src/index.scss b/src/index.scss
index c7973396..8137971b 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -6,7 +6,7 @@ html,
 body,
 #root {
   height: 100%;
-  background: #f5f6fe;
+  background: $lightColor;
 }
 
 * {
@@ -44,6 +44,10 @@ body,
   background-color: $mainColor;
 }
 
+.table-hover tbody tr:hover {
+  background-color: $lightColor;
+}
+
 .react-datepicker__input-container,
 .react-datepicker-wrapper {
   display: block !important;
@@ -88,3 +92,17 @@ body,
 .progress-bar {
   background-color: $mainColor;
 }
+
+.btn-xs-block {
+  @media (max-width: $xsMax) {
+    width: 100%;
+    display: block;
+  }
+}
+
+.btn-md-block {
+  @media (max-width: $mdMax) {
+    width: 100%;
+    display: block;
+  }
+}
diff --git a/src/servers/EditServer.tsx b/src/servers/EditServer.tsx
index 40c52c68..f18c788d 100644
--- a/src/servers/EditServer.tsx
+++ b/src/servers/EditServer.tsx
@@ -24,7 +24,7 @@ export const EditServer = (ServerError: FC) => withSelectedServer<EditServerProp
   return (
     <NoMenuLayout>
       <ServerForm
-        title={<h5 className="mb-0">Edit "{selectedServer.name}"</h5>}
+        title={<h5 className="mb-0">Edit &quot;{selectedServer.name}&quot;</h5>}
         initialValues={selectedServer}
         onSubmit={handleSubmit}
       >
diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx
index 0c4ab5c3..974b928b 100644
--- a/src/servers/Overview.tsx
+++ b/src/servers/Overview.tsx
@@ -9,9 +9,9 @@ import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
 import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
 import { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
 import { VisitsOverview } from '../visits/reducers/visitsOverview';
+import { Versions } from '../utils/helpers/version';
 import { isServerWithId, SelectedServer } from './data';
 import './Overview.scss';
-import { Versions } from '../utils/helpers/version';
 
 interface OverviewConnectProps {
   shortUrlsList: ShortUrlsListState;
diff --git a/src/short-urls/CreateShortUrl.scss b/src/short-urls/CreateShortUrl.scss
index 442bc2a5..81adb310 100644
--- a/src/short-urls/CreateShortUrl.scss
+++ b/src/short-urls/CreateShortUrl.scss
@@ -1,12 +1,5 @@
 @import '../utils/base';
 
-.create-short-url__save-btn {
-  @media (max-width: $xsMax) {
-    width: 100%;
-    display: block;
-  }
-}
-
 .create-short-url .form-group:last-child,
 .create-short-url p:last-child {
   margin-bottom: 0;
diff --git a/src/short-urls/CreateShortUrl.tsx b/src/short-urls/CreateShortUrl.tsx
index aba3ab8c..64c3926f 100644
--- a/src/short-urls/CreateShortUrl.tsx
+++ b/src/short-urls/CreateShortUrl.tsx
@@ -197,7 +197,7 @@ const CreateShortUrl = (
           outline
           color="primary"
           disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
-          className="create-short-url__save-btn"
+          className="btn-xs-block"
         >
           {shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
         </Button>
diff --git a/src/utils/DateRangeRow.scss b/src/utils/DateRangeRow.scss
deleted file mode 100644
index bcd94e78..00000000
--- a/src/utils/DateRangeRow.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@import '../utils/base';
-
-.date-range-row__date-input {
-  @media (max-width: $smMax) {
-    margin-top: .5rem;
-  }
-}
diff --git a/src/utils/DateRangeRow.tsx b/src/utils/DateRangeRow.tsx
index e12a6f26..6f006c28 100644
--- a/src/utils/DateRangeRow.tsx
+++ b/src/utils/DateRangeRow.tsx
@@ -1,6 +1,5 @@
 import moment from 'moment';
 import DateInput from './DateInput';
-import './DateRangeRow.scss';
 
 interface DateRangeRowProps {
   startDate?: moment.Moment | null;
@@ -26,7 +25,7 @@ const DateRangeRow = (
     </div>
     <div className="col-md-6">
       <DateInput
-        className="date-range-row__date-input"
+        className="mt-2 mt-md-0"
         selected={endDate}
         placeholderText="Until"
         isClearable
diff --git a/src/utils/base.scss b/src/utils/base.scss
index 99f65435..a26b7909 100644
--- a/src/utils/base.scss
+++ b/src/utils/base.scss
@@ -10,7 +10,7 @@ $xlgMin: 1200px;
 
 // Colors
 $mainColor: #4696e5;
-$lightHoverColor: #eeeeee;
+$lightColor: #f5f6fe;
 $lightGrey: #dddddd;
 $dangerColor: #dc3545;
 $mediumGrey: #dee2e6;
diff --git a/src/utils/mixins/sticky-cell.scss b/src/utils/mixins/sticky-cell.scss
index 71d48ede..a20f341f 100644
--- a/src/utils/mixins/sticky-cell.scss
+++ b/src/utils/mixins/sticky-cell.scss
@@ -1,4 +1,4 @@
-@import "../base";
+@import '../base';
 
 @mixin sticky-cell() {
   z-index: 1;
diff --git a/src/visits/VisitsStats.scss b/src/visits/VisitsStats.scss
new file mode 100644
index 00000000..8f2c9488
--- /dev/null
+++ b/src/visits/VisitsStats.scss
@@ -0,0 +1,26 @@
+@import '../utils/base';
+
+.visits-stats__nav {
+  position: sticky !important;
+  top: $headerHeight - 1px;
+  z-index: 2;
+}
+
+.visits-stats__nav-link {
+  border-radius: 0 !important;
+  padding-bottom: calc(.5rem - 3px) !important;
+  border-bottom: 3px solid transparent;
+  color: #5d6778;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.visits-stats__nav-link:hover {
+  color: $mainColor !important;
+}
+
+.visits-stats__nav-link.active {
+  border-color: $mainColor;
+  background-color: white !important;
+  color: $mainColor !important;
+}
diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx
index eadd778c..a2ed0fd8 100644
--- a/src/visits/VisitsStats.tsx
+++ b/src/visits/VisitsStats.tsx
@@ -1,14 +1,13 @@
 import { isEmpty, propEq, values } from 'ramda';
 import { useState, useEffect, useMemo, FC } from 'react';
-import { Button, Card, Collapse, Progress } from 'reactstrap';
-import classNames from 'classnames';
+import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
+import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
+import { IconDefinition } from '@fortawesome/fontawesome-common-types';
 import moment from 'moment';
 import DateRangeRow from '../utils/DateRangeRow';
 import Message from '../utils/Message';
 import { formatDate } from '../utils/helpers/date';
-import { useToggle } from '../utils/helpers/hooks';
 import { ShlinkVisitsParams } from '../utils/services/types';
 import SortableBarGraph from './helpers/SortableBarGraph';
 import GraphCard from './helpers/GraphCard';
@@ -17,15 +16,23 @@ import VisitsTable from './VisitsTable';
 import { NormalizedVisit, Stats, VisitsInfo } from './types';
 import OpenMapModalBtn from './helpers/OpenMapModalBtn';
 import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
+import './VisitsStats.scss';
 
 export interface VisitsStatsProps {
-  matchMedia?: (query: string) => MediaQueryList;
   getVisits: (params: Partial<ShlinkVisitsParams>) => void;
   visitsInfo: VisitsInfo;
   cancelGetVisits: () => void;
 }
 
 type HighlightableProps = 'referer' | 'country' | 'city';
+type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
+
+const sections: Record<Section, { title: string; icon: IconDefinition }> = {
+  byTime: { title: 'By time', icon: faCalendarAlt },
+  byContext: { title: 'By context', icon: faChartPie },
+  byLocation: { title: 'By location', icon: faMapMarkedAlt },
+  list: { title: 'List', icon: faList },
+};
 
 const highlightedVisitsToStats = (
   highlightedVisits: NormalizedVisit[],
@@ -42,19 +49,15 @@ const highlightedVisitsToStats = (
 const format = formatDate();
 let selectedBar: string | undefined;
 
-const VisitsStats: FC<VisitsStatsProps> = (
-  { children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia },
-) => {
+const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
   const [ startDate, setStartDate ] = useState<moment.Moment | null>(null);
   const [ endDate, setEndDate ] = useState<moment.Moment | null>(null);
-  const [ showTable, toggleTable ] = useToggle();
-  const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
   const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
   const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
-  const [ isMobileDevice, setIsMobileDevice ] = useState(false);
+  const [ activeSection, setActiveSection ] = useState<Section>('byTime');
+  const onSectionChange = (section: Section) => () => setActiveSection(section);
 
   const { visits, loading, loadingLarge, error, progress } = visitsInfo;
-  const showTableControls = !loading && visits.length > 0;
   const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
   const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
     () => processStatsFromVisits(normalizedVisits),
@@ -62,7 +65,6 @@ const VisitsStats: FC<VisitsStatsProps> = (
   );
   const mapLocations = values(citiesForMap);
 
-  const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
   const setSelectedVisits = (selectedVisits: NormalizedVisit[]) => {
     selectedBar = undefined;
     setHighlightedVisits(selectedVisits);
@@ -81,15 +83,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
     }
   };
 
-  useEffect(() => {
-    determineIsMobileDevice();
-    window.addEventListener('resize', determineIsMobileDevice);
-
-    return () => {
-      cancelGetVisits();
-      window.removeEventListener('resize', determineIsMobileDevice);
-    };
-  }, []);
+  useEffect(() => () => cancelGetVisits(), []);
   useEffect(() => {
     getVisits({ startDate: format(startDate) ?? undefined, endDate: format(endDate) ?? undefined });
   }, [ startDate, endDate ]);
@@ -121,67 +115,106 @@ const VisitsStats: FC<VisitsStatsProps> = (
     }
 
     return (
-      <div className="row">
-        <div className="col-12 mt-4">
-          <LineChartCard
-            title="Visits during time"
-            visits={normalizedVisits}
-            highlightedVisits={highlightedVisits}
-            highlightedLabel={highlightedLabel}
-            setSelectedVisits={setSelectedVisits}
-          />
-        </div>
-        <div className="col-xl-4 col-lg-6 mt-4">
-          <GraphCard title="Operating systems" stats={os} />
-        </div>
-        <div className="col-xl-4 col-lg-6 mt-4">
-          <GraphCard title="Browsers" stats={browsers} />
-        </div>
-        <div className="col-xl-4 mt-4">
-          <SortableBarGraph
-            title="Referrers"
-            stats={referrers}
-            withPagination={false}
-            highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
-            highlightedLabel={highlightedLabel}
-            sortingItems={{
-              name: 'Referrer name',
-              amount: 'Visits amount',
-            }}
-            onClick={highlightVisitsForProp('referer')}
-          />
-        </div>
-        <div className="col-lg-6 mt-4">
-          <SortableBarGraph
-            title="Countries"
-            stats={countries}
-            highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
-            highlightedLabel={highlightedLabel}
-            sortingItems={{
-              name: 'Country name',
-              amount: 'Visits amount',
-            }}
-            onClick={highlightVisitsForProp('country')}
-          />
-        </div>
-        <div className="col-lg-6 mt-4">
-          <SortableBarGraph
-            title="Cities"
-            stats={cities}
-            highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
-            highlightedLabel={highlightedLabel}
-            extraHeaderContent={(activeCities: string[]) =>
-              mapLocations.length > 0 &&
-              <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
-            }
-            sortingItems={{
-              name: 'City name',
-              amount: 'Visits amount',
-            }}
-            onClick={highlightVisitsForProp('city')}
-          />
+      <>
+        <Card className="visits-stats__nav p-0 mt-4 overflow-hidden" body>
+          <Nav pills justified>
+            {Object.entries(sections).map(
+              ([ section, { title, icon }]) => (
+                <NavLink
+                  key={section}
+                  active={activeSection === section}
+                  className="visits-stats__nav-link"
+                  onClick={onSectionChange(section as Section)}
+                >
+                  <FontAwesomeIcon icon={icon} />
+                  <span className="ml-2 d-none d-sm-inline">{title}</span>
+                </NavLink>
+              ),
+            )}
+          </Nav>
+        </Card>
+        <div className="row">
+          {activeSection === 'byTime' && (
+            <div className="col-12 mt-4">
+              <LineChartCard
+                title="Visits during time"
+                visits={normalizedVisits}
+                highlightedVisits={highlightedVisits}
+                highlightedLabel={highlightedLabel}
+                setSelectedVisits={setSelectedVisits}
+              />
+            </div>
+          )}
+          {activeSection === 'byContext' && (
+            <>
+              <div className="col-xl-4 col-lg-6 mt-4">
+                <GraphCard title="Operating systems" stats={os} />
+              </div>
+              <div className="col-xl-4 col-lg-6 mt-4">
+                <GraphCard title="Browsers" stats={browsers} />
+              </div>
+              <div className="col-xl-4 mt-4">
+                <SortableBarGraph
+                  title="Referrers"
+                  stats={referrers}
+                  withPagination={false}
+                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
+                  highlightedLabel={highlightedLabel}
+                  sortingItems={{
+                    name: 'Referrer name',
+                    amount: 'Visits amount',
+                  }}
+                  onClick={highlightVisitsForProp('referer')}
+                />
+              </div>
+            </>
+          )}
+          {activeSection === 'byLocation' && (
+            <>
+              <div className="col-lg-6 mt-4">
+                <SortableBarGraph
+                  title="Countries"
+                  stats={countries}
+                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
+                  highlightedLabel={highlightedLabel}
+                  sortingItems={{
+                    name: 'Country name',
+                    amount: 'Visits amount',
+                  }}
+                  onClick={highlightVisitsForProp('country')}
+                />
+              </div>
+              <div className="col-lg-6 mt-4">
+                <SortableBarGraph
+                  title="Cities"
+                  stats={cities}
+                  highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
+                  highlightedLabel={highlightedLabel}
+                  extraHeaderContent={(activeCities: string[]) =>
+                    mapLocations.length > 0 &&
+                    <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
+                  }
+                  sortingItems={{
+                    name: 'City name',
+                    amount: 'Visits amount',
+                  }}
+                  onClick={highlightVisitsForProp('city')}
+                />
+              </div>
+            </>
+          )}
+          {activeSection === 'list' && (
+            <div className="col-12">
+              <VisitsTable
+                visits={normalizedVisits}
+                selectedVisits={highlightedVisits}
+                setSelectedVisits={setSelectedVisits}
+                isSticky
+              />
+            </div>
+          )}
         </div>
-      </div>
+      </>
     );
   };
 
@@ -200,47 +233,21 @@ const VisitsStats: FC<VisitsStatsProps> = (
               onEndDateChange={setEndDate}
             />
           </div>
-          <div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
-            {showTableControls && (
-              <span className={classNames({ row: isMobileDevice })}>
-                <span className={classNames({ 'col-6': isMobileDevice })}>
-                  <Button outline color="primary" block={isMobileDevice} onClick={toggleTable}>
-                    {showTable ? 'Hide' : 'Show'} table
-                    <FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} className="ml-2" />
-                  </Button>
-                </span>
-                <span className={classNames({ 'col-6': isMobileDevice, 'ml-2': !isMobileDevice })}>
-                  <Button
-                    outline
-                    disabled={highlightedVisits.length === 0}
-                    block={isMobileDevice}
-                    onClick={() => setSelectedVisits([])}
-                  >
-                    Reset selection
-                  </Button>
-                </span>
-              </span>
-            )}
-          </div>
+          {visits.length > 0 && (
+            <div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
+              <Button
+                outline
+                disabled={highlightedVisits.length === 0}
+                className="btn-md-block"
+                onClick={() => setSelectedVisits([])}
+              >
+                Reset selection {highlightedVisits.length > 0 && <>({highlightedVisits.length})</>}
+              </Button>
+            </div>
+          )}
         </div>
       </section>
 
-      {showTableControls && (
-        <Collapse
-          isOpen={showTable}
-          // Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
-          onEntered={setSticky}
-          onExiting={unsetSticky}
-        >
-          <VisitsTable
-            visits={normalizedVisits}
-            selectedVisits={highlightedVisits}
-            setSelectedVisits={setSelectedVisits}
-            isSticky={tableIsSticky}
-          />
-        </Collapse>
-      )}
-
       <section>
         {renderVisitsContent()}
       </section>
diff --git a/src/visits/VisitsTable.scss b/src/visits/VisitsTable.scss
index 54418ddf..ed2cdd7c 100644
--- a/src/visits/VisitsTable.scss
+++ b/src/visits/VisitsTable.scss
@@ -15,7 +15,7 @@
 
   @media (min-width: $mdMin) {
     &.visits-table__sticky {
-      top: $headerHeight - 2px;
+      top: $headerHeight + 40px;
     }
   }
 }
diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx
index 03985b8f..eab3d774 100644
--- a/src/visits/VisitsTable.tsx
+++ b/src/visits/VisitsTable.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useMemo, useState, useRef } from 'react';
 import Moment from 'react-moment';
 import classNames from 'classnames';
 import { min, splitEvery } from 'ramda';
@@ -68,6 +68,7 @@ const VisitsTable = ({
   const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined);
   const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
   const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
+  const isFirstLoad = useRef(true);
 
   const [ page, setPage ] = useState(1);
   const end = page * PAGE_SIZE;
@@ -91,7 +92,12 @@ const VisitsTable = ({
   }, []);
   useEffect(() => {
     setPage(1);
-    setSelectedVisits([]);
+
+    if (isFirstLoad.current) {
+      isFirstLoad.current = false;
+    } else {
+      setSelectedVisits([]);
+    }
   }, [ searchTerm ]);
 
   return (
diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx
index 48209f11..0753ad35 100644
--- a/src/visits/helpers/DefaultChart.tsx
+++ b/src/visits/helpers/DefaultChart.tsx
@@ -1,4 +1,4 @@
-import { useRef } from 'react';
+import { useState } from 'react';
 import { Doughnut, HorizontalBar } from 'react-chartjs-2';
 import { keys, values } from 'ramda';
 import classNames from 'classnames';
@@ -118,7 +118,7 @@ const DefaultChart = (
     }, { ...stats }),
   );
   const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
-  const chartRef = useRef<HorizontalBar | Doughnut>();
+  const [ chartRef, setChartRef ] = useState<HorizontalBar | Doughnut | undefined>()
 
   const options: ChartOptions = {
     legend: { display: false },
@@ -156,7 +156,7 @@ const DefaultChart = (
     <div className="row">
       <div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
         <Component
-          ref={chartRef as any}
+          ref={(element) => setChartRef(element ?? undefined)}
           key={height}
           data={graphData}
           options={options}
@@ -166,7 +166,7 @@ const DefaultChart = (
       </div>
       {!isBarChart && (
         <div className="col-sm-12 col-md-5">
-          {chartRef.current?.chartInstance.generateLegend()}
+          {chartRef?.chartInstance.generateLegend()}
         </div>
       )}
     </div>
diff --git a/src/visits/helpers/LineChartCard.scss b/src/visits/helpers/LineChartCard.scss
index 28272db8..e834aece 100644
--- a/src/visits/helpers/LineChartCard.scss
+++ b/src/visits/helpers/LineChartCard.scss
@@ -4,6 +4,6 @@
   height: 300px !important;
 
   @media (min-width: $mdMin) {
-    height: 350px !important;
+    height: 400px !important;
   }
 }


Test patch:
diff --git a/test/visits/VisitsStats.test.tsx b/test/visits/VisitsStats.test.tsx
index 899bfc3a..3c39bfa3 100644
--- a/test/visits/VisitsStats.test.tsx
+++ b/test/visits/VisitsStats.test.tsx
@@ -1,5 +1,5 @@
 import { shallow, ShallowWrapper } from 'enzyme';
-import { Card, Progress } from 'reactstrap';
+import { Card, NavLink, Progress } from 'reactstrap';
 import { Mock } from 'ts-mockery';
 import VisitStats from '../../src/visits/VisitsStats';
 import Message from '../../src/utils/Message';
@@ -7,6 +7,8 @@ import GraphCard from '../../src/visits/helpers/GraphCard';
 import SortableBarGraph from '../../src/visits/helpers/SortableBarGraph';
 import DateRangeRow from '../../src/utils/DateRangeRow';
 import { Visit, VisitsInfo } from '../../src/visits/types';
+import LineChartCard from '../../src/visits/helpers/LineChartCard';
+import VisitsTable from '../../src/visits/VisitsTable';
 
 describe('<VisitStats />', () => {
   const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
@@ -20,7 +22,6 @@ describe('<VisitStats />', () => {
         getVisits={getVisitsMock}
         visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
         cancelGetVisits={() => {}}
-        matchMedia={() => Mock.of<MediaQueryList>({ matches: false })}
       />,
     );
 
@@ -66,12 +67,24 @@ describe('<VisitStats />', () => {
     expect(message.html()).toContain('There are no visits matching current filter  :(');
   });
 
-  it('renders all graphics when visits are properly loaded', () => {
+  it.each([
+    [ 0, 1, 0 ],
+    [ 1, 3, 0 ],
+    [ 2, 2, 0 ],
+    [ 3, 0, 1 ],
+  ])('renders expected amount of graphics based on active section', (navIndex, expectedGraphics, expectedTables) => {
     const wrapper = createComponent({ loading: false, error: false, visits });
+    const nav = wrapper.find(NavLink).at(navIndex);
+
+    nav.simulate('click');
+
     const graphs = wrapper.find(GraphCard);
     const sortableBarGraphs = wrapper.find(SortableBarGraph);
+    const lineChart = wrapper.find(LineChartCard);
+    const table = wrapper.find(VisitsTable);
 
-    expect(graphs.length + sortableBarGraphs.length).toEqual(5);
+    expect(graphs.length + sortableBarGraphs.length + lineChart.length).toEqual(expectedGraphics);
+    expect(table).toHaveLength(expectedTables);
   });
 
   it('reloads visits when selected dates change', () => {
@@ -88,6 +101,10 @@ describe('<VisitStats />', () => {
 
   it('holds the map button content generator on cities graph extraHeaderContent', () => {
     const wrapper = createComponent({ loading: false, error: false, visits });
+    const locationNav = wrapper.find(NavLink).at(2);
+
+    locationNav.simulate('click');
+
     const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]');
     const extraHeaderContent = citiesGraph.prop('extraHeaderContent');