diff --git a/src/common/MenuLayout.tsx b/src/common/MenuLayout.tsx
index e4a32144..11326487 100644
--- a/src/common/MenuLayout.tsx
+++ b/src/common/MenuLayout.tsx
@@ -65,8 +65,8 @@ const MenuLayout = (
-
- {addTagsVisitsRoute && }
+
+ {addTagsVisitsRoute && }
List short URLs}
diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx
index 485204dd..86397f73 100644
--- a/src/short-urls/ShortUrlsList.tsx
+++ b/src/short-urls/ShortUrlsList.tsx
@@ -2,12 +2,12 @@ import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawe
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { head, keys, values } from 'ramda';
import { FC, useEffect, useState } from 'react';
-import qs from 'qs';
import { RouteComponentProps } from 'react-router';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, OrderDir } from '../utils/utils';
import { SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
+import { parseQuery } from '../utils/helpers/query';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable';
@@ -65,8 +65,8 @@ const ShortUrlsList = (ShortUrlsTable: FC) => boundToMercur
};
useEffect(() => {
- const query = qs.parse(location.search, { ignoreQueryPrefix: true });
- const tags = query.tag ? [ query.tag as string ] : shortUrlsListParams.tags;
+ const { tag } = parseQuery<{ tag?: string }>(location.search);
+ const tags = tag ? [ tag ] : shortUrlsListParams.tags;
refreshList({ page: match.params.page, tags, itemsPerPage: undefined });
diff --git a/src/utils/helpers/query.ts b/src/utils/helpers/query.ts
new file mode 100644
index 00000000..8c59f6eb
--- /dev/null
+++ b/src/utils/helpers/query.ts
@@ -0,0 +1,5 @@
+import qs from 'qs';
+
+export const parseQuery = (search: string) => qs.parse(search, { ignoreQueryPrefix: true }) as unknown as T;
+
+export const stringifyQuery = (query: any): string => qs.stringify(query, { arrayFormat: 'brackets' });
diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx
index 37024b66..4501347a 100644
--- a/src/visits/ShortUrlVisits.tsx
+++ b/src/visits/ShortUrlVisits.tsx
@@ -1,8 +1,8 @@
import { useEffect } from 'react';
-import qs from 'qs';
import { RouteComponentProps } from 'react-router';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { ShlinkVisitsParams } from '../utils/services/types';
+import { parseQuery } from '../utils/helpers/query';
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
import { ShortUrlDetail } from './reducers/shortUrlDetail';
@@ -18,7 +18,7 @@ export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: st
const ShortUrlVisits = boundToMercureHub(({
history: { goBack },
- match,
+ match: { params, url },
location: { search },
shortUrlVisits,
shortUrlDetail,
@@ -26,9 +26,8 @@ const ShortUrlVisits = boundToMercureHub(({
getShortUrlDetail,
cancelGetShortUrlVisits,
}: ShortUrlVisitsProps) => {
- const { params } = match;
const { shortCode } = params;
- const { domain } = qs.parse(search, { ignoreQueryPrefix: true }) as { domain?: string };
+ const { domain } = parseQuery<{ domain?: string }>(search);
const loadVisits = (params: Partial) => getShortUrlVisits(shortCode, { ...params, domain });
@@ -37,7 +36,13 @@ const ShortUrlVisits = boundToMercureHub(({
}, []);
return (
-
+
);
diff --git a/src/visits/TagVisits.tsx b/src/visits/TagVisits.tsx
index 5f2c0244..b0701737 100644
--- a/src/visits/TagVisits.tsx
+++ b/src/visits/TagVisits.tsx
@@ -14,17 +14,16 @@ export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
history: { goBack },
- match,
+ match: { params, url },
getTagVisits,
tagVisits,
cancelGetTagVisits,
}: TagVisitsProps) => {
- const { params } = match;
const { tag } = params;
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
return (
-
+
);
diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx
index 1e9dce11..8816e111 100644
--- a/src/visits/VisitsStats.tsx
+++ b/src/visits/VisitsStats.tsx
@@ -4,6 +4,8 @@ import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
+import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom';
+import { Location } from 'history';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import Message from '../utils/Message';
import { formatIsoDate } from '../utils/helpers/date';
@@ -22,16 +24,18 @@ export interface VisitsStatsProps {
getVisits: (params: Partial) => void;
visitsInfo: VisitsInfo;
cancelGetVisits: () => void;
+ baseUrl: string;
+ domain?: string;
}
type HighlightableProps = 'referer' | 'country' | 'city';
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
-const sections: Record = {
+const sections: Record = {
byTime: { title: 'By time', icon: faCalendarAlt },
- byContext: { title: 'By context', icon: faChartPie },
- byLocation: { title: 'By location', icon: faMapMarkedAlt },
- list: { title: 'List', icon: faList },
+ byContext: { title: 'By context', subPath: 'by-context', icon: faChartPie },
+ byLocation: { title: 'By location', subPath: 'by-location', icon: faMapMarkedAlt },
+ list: { title: 'List', subPath: 'list', icon: faList },
};
const highlightedVisitsToStats = (
@@ -49,13 +53,16 @@ const highlightedVisitsToStats = (
let selectedBar: string | undefined;
const initialInterval: DateInterval = 'last30Days';
-const VisitsStats: FC = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
+const VisitsStats: FC = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => {
const [ dateRange, setDateRange ] = useState(intervalToDateRange(initialInterval));
const [ highlightedVisits, setHighlightedVisits ] = useState([]);
const [ highlightedLabel, setHighlightedLabel ] = useState();
- const [ activeSection, setActiveSection ] = useState('byTime');
- const onSectionChange = (section: Section) => () => setActiveSection(section);
+ const buildSectionUrl = (subPath?: string) => {
+ const query = domain ? `?domain=${domain}` : '';
+
+ return !subPath ? `${baseUrl}${query}` : `${baseUrl}/${subPath}${query}`;
+ };
const { visits, loading, loadingLarge, error, progress } = visitsInfo;
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
@@ -120,12 +127,15 @@ const VisitsStats: FC = ({ children, visitsInfo, getVisits, ca
- {activeSection === 'byTime' && (
-
-
-
- )}
- {activeSection === 'byContext' && (
- <>
+
+
+
+
+
+
+
+
@@ -168,10 +179,9 @@ const VisitsStats: FC = ({ children, visitsInfo, getVisits, ca
onClick={highlightVisitsForProp('referer')}
/>
- >
- )}
- {activeSection === 'byLocation' && (
- <>
+
+
+
= ({ children, visitsInfo, getVisits, ca
onClick={highlightVisitsForProp('city')}
/>
- >
- )}
- {activeSection === 'list' && (
-
-
-
- )}
+
+
+
+
+
+
+
+
+
+
>
);
diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx
index eab3d774..54e16602 100644
--- a/src/visits/VisitsTable.tsx
+++ b/src/visits/VisitsTable.tsx
@@ -19,7 +19,6 @@ interface VisitsTableProps {
visits: NormalizedVisit[];
selectedVisits?: NormalizedVisit[];
setSelectedVisits: (visits: NormalizedVisit[]) => void;
- isSticky?: boolean;
matchMedia?: (query: string) => MediaQueryList;
}
@@ -56,12 +55,9 @@ const VisitsTable = ({
visits,
selectedVisits = [],
setSelectedVisits,
- isSticky = false,
matchMedia = window.matchMedia,
}: VisitsTableProps) => {
- const headerCellsClass = classNames('visits-table__header-cell', {
- 'visits-table__sticky': isSticky,
- });
+ const headerCellsClass = 'visits-table__header-cell visits-table__sticky';
const matchMobile = () => matchMedia('(max-width: 767px)').matches;
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
@@ -105,9 +101,7 @@ const VisitsTable = ({
setSelectedVisits(
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
)}
@@ -183,7 +177,7 @@ const VisitsTable = ({
{resultSet.total > PAGE_SIZE && (
|
-
+ |
', () => {
getVisits={getVisitsMock}
visitsInfo={Mock.of(visitsInfo)}
cancelGetVisits={() => {}}
+ baseUrl={''}
/>,
);
@@ -66,24 +67,15 @@ describe('', () => {
expect(message.html()).toContain('There are no visits matching current filter :(');
});
- 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) => {
+ it('renders expected amount of graphics', () => {
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 + lineChart.length).toEqual(expectedGraphics);
- expect(table).toHaveLength(expectedTables);
+ expect(graphs.length + sortableBarGraphs.length + lineChart.length).toEqual(6);
+ expect(table).toHaveLength(1);
});
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|