mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Added visited URL column on visits table for orphan visits
This commit is contained in:
parent
205e3ffb90
commit
eabd7d9ecb
3 changed files with 39 additions and 12 deletions
|
@ -33,6 +33,7 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
|||
baseUrl={url}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
isOrphanVisits
|
||||
>
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileDownload } fro
|
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||
import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom';
|
||||
import { Location } from 'history';
|
||||
import classNames from 'classnames';
|
||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||
import Message from '../utils/Message';
|
||||
import { formatIsoDate } from '../utils/helpers/date';
|
||||
|
@ -31,6 +32,7 @@ export interface VisitsStatsProps {
|
|||
baseUrl: string;
|
||||
domain?: string;
|
||||
exportCsv: (visits: NormalizedVisit[]) => void;
|
||||
isOrphanVisits?: boolean;
|
||||
}
|
||||
|
||||
interface VisitsNavLinkProps {
|
||||
|
@ -77,7 +79,7 @@ const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title
|
|||
);
|
||||
|
||||
const VisitsStats: FC<VisitsStatsProps> = (
|
||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv },
|
||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv, isOrphanVisits = false },
|
||||
) => {
|
||||
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
|
@ -171,13 +173,13 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||
</Route>
|
||||
|
||||
<Route exact path={`${baseUrl}${sections.byContext.subPath}`}>
|
||||
<div className="col-xl-4 col-lg-6 mt-4">
|
||||
<div className={classNames('mt-4 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
|
||||
<GraphCard title="Operating systems" stats={os} />
|
||||
</div>
|
||||
<div className="col-xl-4 col-lg-6 mt-4">
|
||||
<div className={classNames('mt-4 col-lg-6', { 'col-xl-4': !isOrphanVisits })}>
|
||||
<GraphCard title="Browsers" stats={browsers} />
|
||||
</div>
|
||||
<div className="col-xl-4 mt-4">
|
||||
<div className={classNames('mt-4', { 'col-xl-4': !isOrphanVisits, 'col-lg-6': isOrphanVisits })}>
|
||||
<SortableBarGraph
|
||||
title="Referrers"
|
||||
stats={referrers}
|
||||
|
@ -191,6 +193,18 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||
onClick={highlightVisitsForProp('referer')}
|
||||
/>
|
||||
</div>
|
||||
{isOrphanVisits && (
|
||||
<div className="mt-4 col-lg-6">
|
||||
<SortableBarGraph
|
||||
title="Visited URLs"
|
||||
stats={{}}
|
||||
sortingItems={{
|
||||
visitedUrl: 'Visited URL',
|
||||
amount: 'Visits amount',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Route>
|
||||
|
||||
<Route exact path={`${baseUrl}${sections.byLocation.subPath}`}>
|
||||
|
@ -232,6 +246,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||
visits={normalizedVisits}
|
||||
selectedVisits={highlightedVisits}
|
||||
setSelectedVisits={setSelectedVisits}
|
||||
isOrphanVisits={isOrphanVisits}
|
||||
/>
|
||||
</div>
|
||||
</Route>
|
||||
|
|
|
@ -12,7 +12,7 @@ import SimplePaginator from '../common/SimplePaginator';
|
|||
import SearchField from '../utils/SearchField';
|
||||
import { determineOrderDir, OrderDir } from '../utils/utils';
|
||||
import { prettify } from '../utils/helpers/numbers';
|
||||
import { NormalizedVisit } from './types';
|
||||
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
||||
import './VisitsTable.scss';
|
||||
|
||||
interface VisitsTableProps {
|
||||
|
@ -20,9 +20,10 @@ interface VisitsTableProps {
|
|||
selectedVisits?: NormalizedVisit[];
|
||||
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
||||
matchMedia?: (query: string) => MediaQueryList;
|
||||
isOrphanVisits?: boolean;
|
||||
}
|
||||
|
||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer';
|
||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl';
|
||||
|
||||
interface Order {
|
||||
field?: OrderableFields;
|
||||
|
@ -30,8 +31,10 @@ interface Order {
|
|||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const visitMatchesSearch = ({ browser, os, referer, country, city }: NormalizedVisit, searchTerm: string) =>
|
||||
`${browser} ${os} ${referer} ${country} ${city}`.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const visitMatchesSearch = ({ browser, os, referer, country, city, ...rest }: NormalizedVisit, searchTerm: string) =>
|
||||
`${browser} ${os} ${referer} ${country} ${city} ${(rest as NormalizedOrphanVisit).visitedUrl}`.toLowerCase().includes(
|
||||
searchTerm.toLowerCase(),
|
||||
);
|
||||
const searchVisits = (searchTerm: string, visits: NormalizedVisit[]) =>
|
||||
visits.filter((visit) => visitMatchesSearch(visit, searchTerm));
|
||||
const sortVisits = ({ field, dir }: Order, visits: NormalizedVisit[]) => !field || !dir ? visits : visits.sort(
|
||||
|
@ -39,7 +42,7 @@ const sortVisits = ({ field, dir }: Order, visits: NormalizedVisit[]) => !field
|
|||
const greaterThan = dir === 'ASC' ? 1 : -1;
|
||||
const smallerThan = dir === 'ASC' ? -1 : 1;
|
||||
|
||||
return a[field] > b[field] ? greaterThan : smallerThan;
|
||||
return (a as NormalizedOrphanVisit)[field] > (b as NormalizedOrphanVisit)[field] ? greaterThan : smallerThan;
|
||||
},
|
||||
);
|
||||
const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | undefined, order: Order) => {
|
||||
|
@ -56,6 +59,7 @@ const VisitsTable = ({
|
|||
selectedVisits = [],
|
||||
setSelectedVisits,
|
||||
matchMedia = window.matchMedia,
|
||||
isOrphanVisits = false,
|
||||
}: VisitsTableProps) => {
|
||||
const headerCellsClass = 'visits-table__header-cell visits-table__sticky';
|
||||
const matchMobile = () => matchMedia('(max-width: 767px)').matches;
|
||||
|
@ -132,9 +136,15 @@ const VisitsTable = ({
|
|||
Referrer
|
||||
{renderOrderIcon('referer')}
|
||||
</th>
|
||||
{isOrphanVisits && (
|
||||
<th className={headerCellsClass} onClick={orderByColumn('visitedUrl')}>
|
||||
Visited URL
|
||||
{renderOrderIcon('visitedUrl')}
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={7} className="p-0">
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="p-0">
|
||||
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -142,7 +152,7 @@ const VisitsTable = ({
|
|||
<tbody>
|
||||
{(!resultSet.visitsGroups[page - 1] || resultSet.visitsGroups[page - 1].length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center">
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="text-center">
|
||||
No visits found with current filtering
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -170,6 +180,7 @@ const VisitsTable = ({
|
|||
<td>{visit.browser}</td>
|
||||
<td>{visit.os}</td>
|
||||
<td>{visit.referer}</td>
|
||||
{isOrphanVisits && <td>{(visit as NormalizedOrphanVisit).visitedUrl}</td>}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
@ -177,7 +188,7 @@ const VisitsTable = ({
|
|||
{resultSet.total > PAGE_SIZE && (
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={7} className="visits-table__footer-cell visits-table__sticky">
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="visits-table__footer-cell visits-table__sticky">
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<SimplePaginator
|
||||
|
|
Loading…
Reference in a new issue