mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-07-22 03:31:58 +00:00
Updated to airbnb coding styles
This commit is contained in:
@@ -39,4 +39,4 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc
|
||||
<NonOrphanVisitsHeader nonOrphanVisits={nonOrphanVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [ Topics.visits ]);
|
||||
}, () => [Topics.visits]);
|
||||
|
||||
@@ -44,4 +44,4 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [ Topics.orphanVisits ]);
|
||||
}, () => [Topics.orphanVisits]);
|
||||
|
||||
@@ -59,6 +59,6 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub((
|
||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, (_, params) => [ Topics.shortUrlVisits(params.shortCode) ]);
|
||||
}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]);
|
||||
|
||||
export default ShortUrlVisits;
|
||||
|
||||
@@ -19,7 +19,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
|
||||
const longLink = shortUrl?.longUrl ?? '';
|
||||
const title = shortUrl?.title;
|
||||
|
||||
const renderDate = () => !shortUrl ? <small>Loading...</small> : (
|
||||
const renderDate = () => (!shortUrl ? <small>Loading...</small> : (
|
||||
<span>
|
||||
<b id="created" className="short-url-visits-header__created-at">
|
||||
<Time date={shortUrl.dateCreated} relative />
|
||||
@@ -28,7 +28,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
|
||||
<Time date={shortUrl.dateCreated} />
|
||||
</UncontrolledTooltip>
|
||||
</span>
|
||||
);
|
||||
));
|
||||
const visitsStatsTitle = <>Visits for <ExternalLink href={shortLink} /></>;
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,6 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExpor
|
||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [ Topics.visits ]);
|
||||
}, () => [Topics.visits]);
|
||||
|
||||
export default TagVisits;
|
||||
|
||||
@@ -67,13 +67,13 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||
isOrphanVisits = false,
|
||||
}) => {
|
||||
const { visits, loading, loadingLarge, error, errorData, progress, fallbackInterval } = visitsInfo;
|
||||
const [ initialInterval, setInitialInterval ] = useState<DateInterval>(
|
||||
const [initialInterval, setInitialInterval] = useState<DateInterval>(
|
||||
fallbackInterval ?? settings.visits?.defaultInterval ?? 'last30Days',
|
||||
);
|
||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
||||
const [ visitsFilter, setVisitsFilter ] = useState<VisitsFilter>({});
|
||||
const [dateRange, setDateRange] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
const [highlightedVisits, setHighlightedVisits] = useState<NormalizedVisit[]>([]);
|
||||
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
|
||||
const [visitsFilter, setVisitsFilter] = useState<VisitsFilter>({});
|
||||
const botsSupported = supportsBotVisits(selectedServer);
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
@@ -82,10 +82,10 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||
|
||||
return !subPath ? `${query}` : `${subPath}${query}`;
|
||||
};
|
||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [visits]);
|
||||
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
||||
() => processStatsFromVisits(normalizedVisits),
|
||||
[ normalizedVisits ],
|
||||
[normalizedVisits],
|
||||
);
|
||||
const mapLocations = values(citiesForMap);
|
||||
|
||||
@@ -111,10 +111,10 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||
useEffect(() => {
|
||||
getVisits({ dateRange, filter: visitsFilter }, isFirstLoad.current);
|
||||
isFirstLoad.current = false;
|
||||
}, [ dateRange, visitsFilter ]);
|
||||
}, [dateRange, visitsFilter]);
|
||||
useEffect(() => {
|
||||
fallbackInterval && setInitialInterval(fallbackInterval);
|
||||
}, [ fallbackInterval ]);
|
||||
}, [fallbackInterval]);
|
||||
|
||||
const renderVisitsContent = () => {
|
||||
if (loadingLarge) {
|
||||
@@ -235,10 +235,9 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||
stats={cities}
|
||||
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
|
||||
highlightedLabel={highlightedLabel}
|
||||
extraHeaderContent={(activeCities: string[]) =>
|
||||
mapLocations.length > 0 &&
|
||||
extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && (
|
||||
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
|
||||
}
|
||||
)}
|
||||
sortingItems={{
|
||||
name: 'City name',
|
||||
amount: 'Visits amount',
|
||||
|
||||
@@ -37,7 +37,7 @@ const searchVisits = (searchTerm: string, visits: NormalizedVisit[]) =>
|
||||
visits.filter((visit) => visitMatchesSearch(visit, searchTerm));
|
||||
const sortVisits = (order: VisitsOrder, visits: NormalizedVisit[]) => sortList<NormalizedVisit>(visits, order as any);
|
||||
const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | undefined, order: VisitsOrder) => {
|
||||
const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [ ...allVisits ];
|
||||
const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [...allVisits];
|
||||
const sortedVisits = sortVisits(order, filteredVisits);
|
||||
const total = sortedVisits.length;
|
||||
const visitsGroups = splitEvery(PAGE_SIZE, sortedVisits);
|
||||
@@ -56,12 +56,12 @@ const VisitsTable = ({
|
||||
const headerCellsClass = 'visits-table__header-cell visits-table__sticky';
|
||||
const matchMobile = () => matchMedia('(max-width: 767px)').matches;
|
||||
|
||||
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
|
||||
const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined);
|
||||
const [ order, setOrder ] = useState<VisitsOrder>({});
|
||||
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
||||
const [isMobileDevice, setIsMobileDevice] = useState(matchMobile());
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>(undefined);
|
||||
const [order, setOrder] = useState<VisitsOrder>({});
|
||||
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [searchTerm, order]);
|
||||
const isFirstLoad = useRef(true);
|
||||
const [ page, setPage ] = useState(1);
|
||||
const [page, setPage] = useState(1);
|
||||
const end = page * PAGE_SIZE;
|
||||
const start = end - PAGE_SIZE;
|
||||
const supportsBots = supportsBotVisits(selectedServer);
|
||||
@@ -84,7 +84,7 @@ const VisitsTable = ({
|
||||
|
||||
!isFirstLoad.current && setSelectedVisits([]);
|
||||
isFirstLoad.current = false;
|
||||
}, [ searchTerm ]);
|
||||
}, [searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="table-responsive-md">
|
||||
@@ -159,7 +159,7 @@ const VisitsTable = ({
|
||||
style={{ cursor: 'pointer' }}
|
||||
className={classNames({ 'table-active': isSelected })}
|
||||
onClick={() => setSelectedVisits(
|
||||
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
|
||||
isSelected ? selectedVisits.filter((v) => v !== visit) : [...selectedVisits, visit],
|
||||
)}
|
||||
>
|
||||
<td className="text-center">
|
||||
|
||||
@@ -37,7 +37,7 @@ const generateChartData = (labels: string[], data: number[]): ChartData => ({
|
||||
});
|
||||
|
||||
export const DoughnutChart: FC<DoughnutChartProps> = memo(({ stats }) => {
|
||||
const [ chartRef, setChartRef ] = useState<Chart | undefined>(); // Cannot use useRef here
|
||||
const [chartRef, setChartRef] = useState<Chart | undefined>(); // Cannot use useRef here
|
||||
const labels = keys(stats);
|
||||
const data = values(stats);
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface HorizontalBarChartProps {
|
||||
onClick?: (label: string) => void;
|
||||
}
|
||||
|
||||
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label;
|
||||
const dropLabelIfHidden = (label: string) => (label.startsWith('hidden') ? '' : label);
|
||||
const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
|
||||
const determineHeight = (labels: string[]): number | undefined => labels.length > 20 ? labels.length * 10 : undefined;
|
||||
const determineHeight = (labels: string[]): number | undefined => (labels.length > 20 ? labels.length * 10 : undefined);
|
||||
|
||||
const generateChartDatasets = (
|
||||
data: number[],
|
||||
@@ -34,7 +34,7 @@ const generateChartDatasets = (
|
||||
};
|
||||
|
||||
if (highlightedData.every((value) => value === 0)) {
|
||||
return [ mainDataset ];
|
||||
return [mainDataset];
|
||||
}
|
||||
|
||||
const highlightedDataset: ChartDataset = {
|
||||
@@ -45,7 +45,7 @@ const generateChartDatasets = (
|
||||
borderWidth: 2,
|
||||
};
|
||||
|
||||
return [ mainDataset, highlightedDataset ];
|
||||
return [mainDataset, highlightedDataset];
|
||||
};
|
||||
const generateChartData = (
|
||||
labels: string[],
|
||||
@@ -58,7 +58,7 @@ const generateChartData = (
|
||||
});
|
||||
|
||||
type ClickedCharts = [{ index: number }] | [];
|
||||
const chartElementAtEvent = (labels: string[], onClick?: (label: string) => void) => ([ chart ]: ClickedCharts) => {
|
||||
const chartElementAtEvent = (labels: string[], onClick?: (label: string) => void) => ([chart]: ClickedCharts) => {
|
||||
if (!onClick || !chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ const determineInitialStep = (oldestVisitDate: string): Step => {
|
||||
const now = new Date();
|
||||
const oldestDate = parseISO(oldestVisitDate);
|
||||
const matcher = cond<never, Step | undefined>([
|
||||
[ () => differenceInDays(now, oldestDate) <= 2, always<Step>('hourly') ], // Less than 2 days
|
||||
[ () => differenceInMonths(now, oldestDate) <= 1, always<Step>('daily') ], // Between 2 days and 1 month
|
||||
[ () => differenceInMonths(now, oldestDate) <= 6, always<Step>('weekly') ], // Between 1 and 6 months
|
||||
[() => differenceInDays(now, oldestDate) <= 2, always<Step>('hourly')], // Less than 2 days
|
||||
[() => differenceInMonths(now, oldestDate) <= 1, always<Step>('daily')], // Between 2 days and 1 month
|
||||
[() => differenceInMonths(now, oldestDate) <= 6, always<Step>('weekly')], // Between 1 and 6 months
|
||||
]);
|
||||
|
||||
return matcher() ?? 'monthly';
|
||||
@@ -126,12 +126,12 @@ const generateLabelsAndGroupedVisits = (
|
||||
skipNoElements: boolean,
|
||||
): [string[], number[]] => {
|
||||
if (skipNoElements) {
|
||||
return [ Object.keys(groupedVisitsWithGaps), Object.values(groupedVisitsWithGaps) ];
|
||||
return [Object.keys(groupedVisitsWithGaps), Object.values(groupedVisitsWithGaps)];
|
||||
}
|
||||
|
||||
const labels = generateLabels(step, visits);
|
||||
|
||||
return [ labels, fillTheGaps(groupedVisitsWithGaps, labels) ];
|
||||
return [labels, fillTheGaps(groupedVisitsWithGaps, labels)];
|
||||
};
|
||||
|
||||
const generateDataset = (data: number[], label: string, color: string): ChartDataset => ({
|
||||
@@ -149,7 +149,7 @@ const chartElementAtEvent = (
|
||||
labels: string[],
|
||||
datasetsByPoint: Record<string, NormalizedVisit[]>,
|
||||
setSelectedVisits?: (visits: NormalizedVisit[]) => void,
|
||||
) => ([ chart ]: [{ index: number }]) => {
|
||||
) => ([chart]: [{ index: number }]) => {
|
||||
if (!setSelectedVisits || !chart) {
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ const chartElementAtEvent = (
|
||||
setSelectedVisits([]);
|
||||
selectedLabel = null;
|
||||
} else {
|
||||
setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] || []);
|
||||
setSelectedVisits(labels[index] ? datasetsByPoint[labels[index]] : []);
|
||||
selectedLabel = labels[index] ?? null;
|
||||
}
|
||||
};
|
||||
@@ -168,31 +168,31 @@ const chartElementAtEvent = (
|
||||
const LineChartCard = (
|
||||
{ title, visits, highlightedVisits, highlightedLabel = 'Selected', setSelectedVisits }: LineChartCardProps,
|
||||
) => {
|
||||
const [ step, setStep ] = useState<Step>(
|
||||
const [step, setStep] = useState<Step>(
|
||||
visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly',
|
||||
);
|
||||
const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true);
|
||||
const [skipNoVisits, toggleSkipNoVisits] = useToggle(true);
|
||||
|
||||
const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [ step, visits ]);
|
||||
const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ step, visits ]);
|
||||
const [ labels, groupedVisits ] = useMemo(
|
||||
const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [step, visits]);
|
||||
const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [step, visits]);
|
||||
const [labels, groupedVisits] = useMemo(
|
||||
() => generateLabelsAndGroupedVisits(visits, groupedVisitsWithGaps, step, skipNoVisits),
|
||||
[ visits, step, skipNoVisits ],
|
||||
[visits, step, skipNoVisits],
|
||||
);
|
||||
const groupedHighlighted = useMemo(
|
||||
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
|
||||
[ highlightedVisits, step, labels ],
|
||||
[highlightedVisits, step, labels],
|
||||
);
|
||||
const generateChartDatasets = (): ChartDataset[] => {
|
||||
const mainDataset = generateDataset(groupedVisits, 'Visits', MAIN_COLOR);
|
||||
|
||||
if (highlightedVisits.length === 0) {
|
||||
return [ mainDataset ];
|
||||
return [mainDataset];
|
||||
}
|
||||
|
||||
const highlightedDataset = generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR);
|
||||
|
||||
return [ mainDataset, highlightedDataset ];
|
||||
return [mainDataset, highlightedDataset];
|
||||
};
|
||||
const generateChartData = (): ChartData => ({ labels, datasets: generateChartDatasets() });
|
||||
|
||||
@@ -238,7 +238,7 @@ const LineChartCard = (
|
||||
Group by
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end>
|
||||
{Object.entries(STEPS_MAP).map(([ value, menuText ]) => (
|
||||
{Object.entries(STEPS_MAP).map(([value, menuText]) => (
|
||||
<DropdownItem key={value} active={step === value} onClick={() => setStep(value as Step)}>
|
||||
{menuText}
|
||||
</DropdownItem>
|
||||
|
||||
@@ -17,9 +17,9 @@ interface SortableBarChartCardProps extends Omit<HorizontalBarChartProps, 'max'>
|
||||
extraHeaderContent?: Function;
|
||||
}
|
||||
|
||||
const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value; // eslint-disable-line @typescript-eslint/no-unsafe-return
|
||||
const pickKeyFromPair = ([ key ]: StatsRow) => key;
|
||||
const pickValueFromPair = ([ , value ]: StatsRow) => value;
|
||||
const toLowerIfString = (value: any) => (type(value) === 'String' ? toLower(value) : value);
|
||||
const pickKeyFromPair = ([key]: StatsRow) => key;
|
||||
const pickValueFromPair = ([, value]: StatsRow) => value;
|
||||
|
||||
export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||
stats,
|
||||
@@ -30,15 +30,15 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||
withPagination = true,
|
||||
...rest
|
||||
}) => {
|
||||
const [ order, setOrder ] = useState<Order<string>>({});
|
||||
const [ currentPage, setCurrentPage ] = useState(1);
|
||||
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
||||
const [order, setOrder] = useState<Order<string>>({});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(50);
|
||||
|
||||
const getSortedPairsForStats = (stats: Stats, sortingItems: Record<string, string>) => {
|
||||
const pairs = toPairs(stats);
|
||||
const getSortedPairsForStats = (statsToSort: Stats, sorting: Record<string, string>) => {
|
||||
const pairs = toPairs(statsToSort);
|
||||
const sortedPairs = !order.field ? pairs : sortBy(
|
||||
pipe<StatsRow, string | number, string | number>(
|
||||
order.field === Object.keys(sortingItems)[0] ? pickKeyFromPair : pickValueFromPair,
|
||||
order.field === Object.keys(sorting)[0] ? pickKeyFromPair : pickValueFromPair,
|
||||
toLowerIfString,
|
||||
),
|
||||
pairs,
|
||||
@@ -56,16 +56,16 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||
const firstPageLength = pages[0].length;
|
||||
|
||||
// Using the "hidden" key, the chart will just replace the label by an empty string
|
||||
return [ ...page, ...rangeOf(firstPageLength - page.length, (i): StatsRow => [ `hidden_${i}`, 0 ]) ];
|
||||
return [...page, ...rangeOf(firstPageLength - page.length, (i): StatsRow => [`hidden_${i}`, 0])];
|
||||
};
|
||||
const renderPagination = (pagesCount: number) =>
|
||||
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
|
||||
const determineStats = (stats: Stats, highlightedStats: Stats | undefined, sortingItems: Record<string, string>) => {
|
||||
const sortedPairs = getSortedPairsForStats(stats, sortingItems);
|
||||
const determineStats = (statsToSort: Stats, sorting: Record<string, string>, theHighlightedStats?: Stats) => {
|
||||
const sortedPairs = getSortedPairsForStats(statsToSort, sorting);
|
||||
const sortedKeys = sortedPairs.map(pickKeyFromPair);
|
||||
// The highlighted stats have to be ordered based on the regular stats, not on its own values
|
||||
const sortedHighlightedPairs = highlightedStats && toPairs(
|
||||
{ ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...highlightedStats },
|
||||
const sortedHighlightedPairs = theHighlightedStats && toPairs(
|
||||
{ ...zipObj(sortedKeys, sortedKeys.map(() => 0)), ...theHighlightedStats },
|
||||
);
|
||||
|
||||
if (sortedPairs.length <= itemsPerPage) {
|
||||
@@ -88,8 +88,8 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||
|
||||
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
|
||||
stats,
|
||||
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
||||
sortingItems,
|
||||
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
||||
);
|
||||
const activeCities = Object.keys(currentPageStats);
|
||||
const computeTitle = () => (
|
||||
@@ -111,10 +111,10 @@ export const SortableBarChartCard: FC<SortableBarChartCardProps> = ({
|
||||
<div className="float-end">
|
||||
<PaginationDropdown
|
||||
toggleClassName="btn-sm p-0 me-3"
|
||||
ranges={[ 50, 100, 200, 500 ]}
|
||||
ranges={[50, 100, 200, 500]}
|
||||
value={itemsPerPage}
|
||||
setValue={(itemsPerPage) => {
|
||||
setItemsPerPage(itemsPerPage);
|
||||
setValue={(value) => {
|
||||
setItemsPerPage(value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap';
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import { CityStats } from '../types';
|
||||
import MapModal from './MapModal';
|
||||
@@ -14,9 +14,9 @@ interface OpenMapModalBtnProps {
|
||||
}
|
||||
|
||||
const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapModalBtnProps) => {
|
||||
const [ mapIsOpened, , openMap, closeMap ] = useToggle();
|
||||
const [ dropdownIsOpened, toggleDropdown, openDropdown ] = useToggle();
|
||||
const [ locationsToShow, setLocationsToShow ] = useState<CityStats[]>([]);
|
||||
const [mapIsOpened, , openMap, closeMap] = useToggle();
|
||||
const [dropdownIsOpened, toggleDropdown, openDropdown] = useToggle();
|
||||
const [locationsToShow, setLocationsToShow] = useState<CityStats[]>([]);
|
||||
const buttonRef = useRef<HTMLElement>();
|
||||
|
||||
const filterLocations = (cities: CityStats[]) => cities.filter(({ cityName }) => activeCities.includes(cityName));
|
||||
@@ -37,9 +37,9 @@ const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapMo
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="btn btn-link open-map-modal-btn__btn" ref={buttonRef as any} onClick={onClick}>
|
||||
<Button color="link" className="open-map-modal-btn__btn" ref={buttonRef as any} onClick={onClick}>
|
||||
<FontAwesomeIcon icon={mapIcon} />
|
||||
</button>
|
||||
</Button>
|
||||
<UncontrolledTooltip placement="left" target={(() => buttonRef.current) as any}>Show in map</UncontrolledTooltip>
|
||||
<Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown} inNavbar>
|
||||
<DropdownMenu end>
|
||||
|
||||
@@ -11,7 +11,7 @@ const PARALLEL_REQUESTS_COUNT = 4;
|
||||
const PARALLEL_STARTING_PAGE = 2;
|
||||
|
||||
const isLastPage = ({ currentPage, pagesCount }: ShlinkPaginator): boolean => currentPage >= pagesCount;
|
||||
const calcProgress = (total: number, current: number): number => current * 100 / total;
|
||||
const calcProgress = (total: number, current: number): number => (current * 100) / total;
|
||||
|
||||
type VisitsLoader = (page: number, itemsPerPage: number) => Promise<ShlinkVisits>;
|
||||
type LastVisitLoader = () => Promise<Visit | undefined>;
|
||||
@@ -73,7 +73,7 @@ export const getVisitsWithLoader = async <T extends Action<string> & { visits: V
|
||||
};
|
||||
|
||||
try {
|
||||
const [ visits, lastVisit ] = await Promise.all([ loadVisits(), lastVisitLoader() ]);
|
||||
const [visits, lastVisit] = await Promise.all([loadVisits(), lastVisitLoader()]);
|
||||
|
||||
dispatch(
|
||||
!visits.length && lastVisit
|
||||
|
||||
@@ -14,7 +14,6 @@ import { isBetween } from '../../utils/helpers/date';
|
||||
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_NON_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_START';
|
||||
export const GET_NON_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_ERROR';
|
||||
export const GET_NON_ORPHAN_VISITS = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS';
|
||||
@@ -22,7 +21,6 @@ export const GET_NON_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_NON_ORPHAN_V
|
||||
export const GET_NON_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_CANCEL';
|
||||
export const GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED';
|
||||
export const GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface NonOrphanVisitsAction extends Action<string> {
|
||||
visits: Visit[];
|
||||
@@ -59,7 +57,7 @@ export default buildReducer<VisitsInfo, NonOrphanVisitsCombinedAction>({
|
||||
.filter(({ visit }) => isBetween(visit.date, startDate, endDate))
|
||||
.map(({ visit }) => visit);
|
||||
|
||||
return { ...state, visits: [ ...newVisits, ...visits ] };
|
||||
return { ...state, visits: [...newVisits, ...visits] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
@@ -67,10 +65,10 @@ export const getNonOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder)
|
||||
query: ShlinkVisitsParams = {},
|
||||
doIntervalFallback = false,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { getNonOrphanVisits } = buildShlinkApiClient(getState);
|
||||
const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) =>
|
||||
getNonOrphanVisits({ ...query, page, itemsPerPage });
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getNonOrphanVisits);
|
||||
shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage });
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits);
|
||||
const shouldCancel = () => getState().orphanVisits.cancelLoad;
|
||||
const extraFinishActionData: Partial<NonOrphanVisitsAction> = { query };
|
||||
const actionMap = {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { isBetween } from '../../utils/helpers/date';
|
||||
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_ORPHAN_VISITS_START';
|
||||
export const GET_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_ORPHAN_VISITS_ERROR';
|
||||
export const GET_ORPHAN_VISITS = 'shlink/orphanVisits/GET_ORPHAN_VISITS';
|
||||
@@ -25,7 +24,6 @@ export const GET_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_ORPHAN_VISITS_LA
|
||||
export const GET_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_CANCEL';
|
||||
export const GET_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_ORPHAN_VISITS_PROGRESS_CHANGED';
|
||||
export const GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_ORPHAN_VISITS_FALLBACK_TO_INTERVAL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface OrphanVisitsAction extends Action<string> {
|
||||
visits: Visit[];
|
||||
@@ -62,7 +60,7 @@ export default buildReducer<VisitsInfo, OrphanVisitsCombinedAction>({
|
||||
.filter(({ visit, shortUrl }) => !shortUrl && isBetween(visit.date, startDate, endDate))
|
||||
.map(({ visit }) => visit);
|
||||
|
||||
return { ...state, visits: [ ...newVisits, ...visits ] };
|
||||
return { ...state, visits: [...newVisits, ...visits] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
@@ -74,14 +72,14 @@ export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
|
||||
orphanVisitsType?: OrphanVisitType,
|
||||
doIntervalFallback = false,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { getOrphanVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage })
|
||||
const { getOrphanVisits: getVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits({ ...query, page, itemsPerPage })
|
||||
.then((result) => {
|
||||
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
|
||||
|
||||
return { ...result, data: visits };
|
||||
});
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getOrphanVisits);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getVisits);
|
||||
const shouldCancel = () => getState().orphanVisits.cancelLoad;
|
||||
const extraFinishActionData: Partial<OrphanVisitsAction> = { query };
|
||||
const actionMap = {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { isBetween } from '../../utils/helpers/date';
|
||||
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
||||
export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
|
||||
export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
|
||||
@@ -19,7 +18,6 @@ export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_V
|
||||
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
|
||||
export const GET_SHORT_URL_VISITS_PROGRESS_CHANGED = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_PROGRESS_CHANGED';
|
||||
export const GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_FALLBACK_TO_INTERVAL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface ShortUrlVisits extends VisitsInfo, ShortUrlIdentifier {}
|
||||
|
||||
@@ -71,7 +69,7 @@ export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
|
||||
)
|
||||
.map(({ visit }) => visit);
|
||||
|
||||
return newVisits.length === 0 ? state : { ...state, visits: [ ...newVisits, ...visits ] };
|
||||
return newVisits.length === 0 ? state : { ...state, visits: [...newVisits, ...visits] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
@@ -80,14 +78,14 @@ export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder)
|
||||
query: ShlinkVisitsParams = {},
|
||||
doIntervalFallback = false,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { getShortUrlVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits(
|
||||
const { getShortUrlVisits: shlinkGetShortUrlVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits(
|
||||
shortCode,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(
|
||||
doIntervalFallback,
|
||||
async (params) => getShortUrlVisits(shortCode, { ...params, domain: query.domain }),
|
||||
async (params) => shlinkGetShortUrlVisits(shortCode, { ...params, domain: query.domain }),
|
||||
);
|
||||
const shouldCancel = () => getState().shortUrlVisits.cancelLoad;
|
||||
const extraFinishActionData: Partial<ShortUrlVisitsAction> = { shortCode, query, domain: query.domain };
|
||||
|
||||
@@ -9,7 +9,6 @@ import { isBetween } from '../../utils/helpers/date';
|
||||
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START';
|
||||
export const GET_TAG_VISITS_ERROR = 'shlink/tagVisits/GET_TAG_VISITS_ERROR';
|
||||
export const GET_TAG_VISITS = 'shlink/tagVisits/GET_TAG_VISITS';
|
||||
@@ -17,7 +16,6 @@ export const GET_TAG_VISITS_LARGE = 'shlink/tagVisits/GET_TAG_VISITS_LARGE';
|
||||
export const GET_TAG_VISITS_CANCEL = 'shlink/tagVisits/GET_TAG_VISITS_CANCEL';
|
||||
export const GET_TAG_VISITS_PROGRESS_CHANGED = 'shlink/tagVisits/GET_TAG_VISITS_PROGRESS_CHANGED';
|
||||
export const GET_TAG_VISITS_FALLBACK_TO_INTERVAL = 'shlink/tagVisits/GET_TAG_VISITS_FALLBACK_TO_INTERVAL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface TagVisits extends VisitsInfo {
|
||||
tag: string;
|
||||
@@ -60,7 +58,7 @@ export default buildReducer<TagVisits, TagsVisitsCombinedAction>({
|
||||
.filter(({ shortUrl, visit }) => shortUrl?.tags.includes(tag) && isBetween(visit.date, startDate, endDate))
|
||||
.map(({ visit }) => visit);
|
||||
|
||||
return { ...state, visits: [ ...newVisits, ...visits ] };
|
||||
return { ...state, visits: [...newVisits, ...visits] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
@@ -69,12 +67,12 @@ export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||
query: ShlinkVisitsParams = {},
|
||||
doIntervalFallback = false,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { getTagVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits(
|
||||
const { getTagVisits: getVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||
tag,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getTagVisits(tag, params));
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(tag, params));
|
||||
const shouldCancel = () => getState().tagVisits.cancelLoad;
|
||||
const extraFinishActionData: Partial<TagVisitsAction> = { tag, query };
|
||||
const actionMap = {
|
||||
|
||||
@@ -6,11 +6,9 @@ import { buildReducer } from '../../utils/helpers/redux';
|
||||
import { groupNewVisitsByType } from '../types/helpers';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_OVERVIEW_START = 'shlink/visitsOverview/GET_OVERVIEW_START';
|
||||
export const GET_OVERVIEW_ERROR = 'shlink/visitsOverview/GET_OVERVIEW_ERROR';
|
||||
export const GET_OVERVIEW = 'shlink/visitsOverview/GET_OVERVIEW';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export interface VisitsOverview {
|
||||
visitsCount: number;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { hasValue } from '../../utils/utils';
|
||||
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
|
||||
import { isNormalizedOrphanVisit, isOrphanVisit } from '../types/helpers';
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) =>
|
||||
!isNil(visit) && hasValue(visit[propertyName]);
|
||||
|
||||
@@ -46,10 +47,10 @@ const updateCitiesForMapForVisit = (citiesForMapStats: Record<string, CityStats>
|
||||
const currentCity = citiesForMapStats[city] || {
|
||||
cityName: city,
|
||||
count: 0,
|
||||
latLong: [ optionalNumericToNumber(latitude), optionalNumericToNumber(longitude) ],
|
||||
latLong: [optionalNumericToNumber(latitude), optionalNumericToNumber(longitude)],
|
||||
};
|
||||
|
||||
currentCity.count++;
|
||||
currentCity.count += 1;
|
||||
|
||||
citiesForMapStats[city] = currentCity;
|
||||
};
|
||||
|
||||
@@ -19,26 +19,26 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter');
|
||||
bottle.decorator('ShortUrlVisits', connect(
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
['shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
['getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter');
|
||||
bottle.decorator('TagVisits', connect(
|
||||
[ 'tagVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
['tagVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
||||
bottle.decorator('OrphanVisits', connect(
|
||||
[ 'orphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
['orphanVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
['getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter');
|
||||
bottle.decorator('NonOrphanVisits', connect(
|
||||
[ 'nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
['nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
['getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
// Services
|
||||
|
||||
@@ -3,10 +3,10 @@ import { formatIsoDate } from '../../utils/helpers/date';
|
||||
import { ShlinkVisitsParams } from '../../api/types';
|
||||
import { CreateVisit, NormalizedOrphanVisit, NormalizedVisit, OrphanVisit, Stats, Visit, VisitsParams } from './index';
|
||||
|
||||
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
|
||||
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => !!(visit as OrphanVisit).visitedUrl;
|
||||
|
||||
export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is NormalizedOrphanVisit =>
|
||||
visit.hasOwnProperty('visitedUrl');
|
||||
!!(visit as NormalizedOrphanVisit).visitedUrl;
|
||||
|
||||
export interface GroupedNewVisits {
|
||||
orphanVisits: CreateVisit[];
|
||||
@@ -14,7 +14,7 @@ export interface GroupedNewVisits {
|
||||
}
|
||||
|
||||
export const groupNewVisitsByType = pipe(
|
||||
groupBy((newVisit: CreateVisit) => isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits'),
|
||||
groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits')),
|
||||
// @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props
|
||||
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user