Added filtering by type to orphan visits

This commit is contained in:
Alejandro Celaya 2021-03-28 17:45:47 +02:00
parent 6d887ec4a8
commit d6bb718672
5 changed files with 121 additions and 13 deletions

View file

@ -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>
);
};

View file

@ -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">

View 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>
);

View file

@ -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;

View file

@ -0,0 +1,56 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { DropdownItem } from 'reactstrap';
import { OrphanVisitType } from '../../../src/visits/types';
import { OrphanVisitTypeDropdown } from '../../../src/visits/helpers/OrphanVisitTypeDropdown';
describe('<OrphanVisitTypeDropdown />', () => {
let wrapper: ShallowWrapper;
const onChange = jest.fn();
const createWrapper = (selected?: OrphanVisitType) => {
wrapper = shallow(<OrphanVisitTypeDropdown text="The text" selected={selected} onChange={onChange} />);
return wrapper;
};
beforeEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('has provided text', () => {
const wrapper = createWrapper();
expect(wrapper.prop('text')).toEqual('The text');
});
it.each([
[ 'base_url' as OrphanVisitType, 0, 1 ],
[ 'invalid_short_url' as OrphanVisitType, 1, 1 ],
[ 'regular_404' as OrphanVisitType, 2, 1 ],
[ undefined, -1, 0 ],
])('sets expected item as active', (selected, expectedSelectedIndex, expectedActiveItems) => {
const wrapper = createWrapper(selected);
const items = wrapper.find(DropdownItem);
const activeItem = items.filterWhere((item) => !!item.prop('active'));
expect.assertions(expectedActiveItems + 1);
expect(activeItem).toHaveLength(expectedActiveItems);
items.forEach((item, index) => {
if (item.prop('active')) {
expect(index).toEqual(expectedSelectedIndex);
}
});
});
it.each([
[ 0, 'base_url' ],
[ 1, 'invalid_short_url' ],
[ 2, 'regular_404' ],
[ 4, undefined ],
])('invokes onChange with proper type when an item is clicked', (index, expectedType) => {
const wrapper = createWrapper();
const itemToClick = wrapper.find(DropdownItem).at(index);
itemToClick.simulate('click');
expect(onChange).toHaveBeenCalledWith(expectedType);
});
});