mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-03 14:21:49 +00:00
Added filtering by type to orphan visits
This commit is contained in:
@@ -7,16 +7,20 @@ export interface DropdownBtnProps {
|
||||
text: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
dropdownClassName?: string;
|
||||
right?: boolean;
|
||||
}
|
||||
|
||||
export const DropdownBtn: FC<DropdownBtnProps> = ({ text, disabled = false, className = '', children }) => {
|
||||
export const DropdownBtn: FC<DropdownBtnProps> = (
|
||||
{ text, disabled = false, className = '', children, dropdownClassName, right = false },
|
||||
) => {
|
||||
const [ isOpen, toggle ] = useToggle();
|
||||
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`;
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
|
||||
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled} className={dropdownClassName}>
|
||||
<DropdownToggle caret className={toggleClasses} color="primary">{text}</DropdownToggle>
|
||||
<DropdownMenu className="w-100">{children}</DropdownMenu>
|
||||
<DropdownMenu className="w-100" right={right}>{children}</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { countBy, isEmpty, prop, propEq, values } from 'ramda';
|
||||
import { countBy, filter, isEmpty, pipe, prop, propEq, values } from 'ramda';
|
||||
import { useState, useEffect, useMemo, FC } from 'react';
|
||||
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -19,9 +19,10 @@ import SortableBarGraph from './helpers/SortableBarGraph';
|
||||
import GraphCard from './helpers/GraphCard';
|
||||
import LineChartCard from './helpers/LineChartCard';
|
||||
import VisitsTable from './VisitsTable';
|
||||
import { NormalizedVisit, Stats, VisitsInfo } from './types';
|
||||
import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, Stats, Visit, VisitsInfo } from './types';
|
||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
||||
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
||||
import './VisitsStats.scss';
|
||||
|
||||
export interface VisitsStatsProps {
|
||||
@@ -54,6 +55,11 @@ const sections: Record<Section, VisitsNavLinkProps> = {
|
||||
const highlightedVisitsToStats = (highlightedVisits: NormalizedVisit[], property: HighlightableProps): Stats =>
|
||||
countBy(prop(property), highlightedVisits);
|
||||
|
||||
const normalizeAndFilterVisits = (visits: Visit[], type: OrphanVisitType | undefined) => pipe(
|
||||
normalizeVisits,
|
||||
filter((normalizedVisit) => type === undefined || (normalizedVisit as NormalizedOrphanVisit).type === type),
|
||||
)(visits);
|
||||
|
||||
let selectedBar: string | undefined;
|
||||
|
||||
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
|
||||
@@ -76,6 +82,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
||||
const [ orphanVisitType, setOrphanVisitType ] = useState<OrphanVisitType | undefined>();
|
||||
|
||||
const buildSectionUrl = (subPath?: string) => {
|
||||
const query = domain ? `?domain=${domain}` : '';
|
||||
@@ -83,7 +90,10 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
|
||||
};
|
||||
const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo;
|
||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||
const normalizedVisits = useMemo(
|
||||
() => normalizeAndFilterVisits(visits, orphanVisitType),
|
||||
[ visits, orphanVisitType ],
|
||||
);
|
||||
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
||||
() => processStatsFromVisits(normalizedVisits),
|
||||
[ normalizedVisits ],
|
||||
@@ -256,12 +266,24 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||
<section className="mt-4">
|
||||
<div className="row flex-md-row-reverse">
|
||||
<div className="col-lg-7 col-xl-6">
|
||||
<DateRangeSelector
|
||||
disabled={loading}
|
||||
initialDateRange={initialInterval}
|
||||
defaultText="All visits"
|
||||
onDatesChange={setDateRange}
|
||||
/>
|
||||
<div className="d-md-flex">
|
||||
<div className="flex-fill">
|
||||
<DateRangeSelector
|
||||
disabled={loading}
|
||||
initialDateRange={initialInterval}
|
||||
defaultText="All visits"
|
||||
onDatesChange={setDateRange}
|
||||
/>
|
||||
</div>
|
||||
{isOrphanVisits && (
|
||||
<OrphanVisitTypeDropdown
|
||||
text="Filter by type"
|
||||
className="ml-0 ml-md-2 mt-4 mt-md-0"
|
||||
selected={orphanVisitType}
|
||||
onChange={setOrphanVisitType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{visits.length > 0 && (
|
||||
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
|
||||
|
||||
26
src/visits/helpers/OrphanVisitTypeDropdown.tsx
Normal file
26
src/visits/helpers/OrphanVisitTypeDropdown.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { OrphanVisitType } from '../types';
|
||||
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||
|
||||
interface OrphanVisitTypeDropdownProps {
|
||||
onChange: (type: OrphanVisitType | undefined) => void;
|
||||
selected?: OrphanVisitType | undefined;
|
||||
className?: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const OrphanVisitTypeDropdown = ({ onChange, selected, text, className }: OrphanVisitTypeDropdownProps) => (
|
||||
<DropdownBtn text={text} dropdownClassName={className} className="mr-3" right>
|
||||
<DropdownItem active={selected === 'base_url'} onClick={() => onChange('base_url')}>
|
||||
Base URL
|
||||
</DropdownItem>
|
||||
<DropdownItem active={selected === 'invalid_short_url'} onClick={() => onChange('invalid_short_url')}>
|
||||
Invalid short URL
|
||||
</DropdownItem>
|
||||
<DropdownItem active={selected === 'regular_404'} onClick={() => onChange('regular_404')}>
|
||||
Regular 404
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem onClick={() => onChange(undefined)}><i>Clear selection</i></DropdownItem>
|
||||
</DropdownBtn>
|
||||
);
|
||||
@@ -20,7 +20,7 @@ export interface VisitsLoadFailedAction extends Action<string> {
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
type OrphanVisitType = 'base_url' | 'invalid_short_url' | 'regular_404';
|
||||
export type OrphanVisitType = 'base_url' | 'invalid_short_url' | 'regular_404';
|
||||
|
||||
interface VisitLocation {
|
||||
countryCode: string | null;
|
||||
|
||||
Reference in New Issue
Block a user