mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 06:47:29 +03:00
Merge pull request #775 from acelaya-forks/feature/shlink-updates
Feature/shlink updates
This commit is contained in:
commit
35fcd20123
33 changed files with 113 additions and 237 deletions
|
@ -18,7 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* [#736](https://github.com/shlinkio/shlink-web-client/issues/736) Removed support for cards mode in tags. Only tags table is supported now.
|
* [#736](https://github.com/shlinkio/shlink-web-client/issues/736) Removed cards mode in tags. Only table mode is supported now.
|
||||||
|
* [#774](https://github.com/shlinkio/shlink-web-client/issues/774) Dropped support for Shlink older than 2.8.0.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
import { DeleteServerButtonProps } from '../servers/DeleteServerButton';
|
||||||
import { isServerWithId, SelectedServer } from '../servers/data';
|
import { isServerWithId, SelectedServer } from '../servers/data';
|
||||||
import { supportsDomainRedirects } from '../utils/helpers/features';
|
|
||||||
import './AsideMenu.scss';
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
export interface AsideMenuProps {
|
export interface AsideMenuProps {
|
||||||
|
@ -40,7 +39,6 @@ export const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
const hasId = isServerWithId(selectedServer);
|
const hasId = isServerWithId(selectedServer);
|
||||||
const serverId = hasId ? selectedServer.id : '';
|
const serverId = hasId ? selectedServer.id : '';
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const addManageDomainsLink = supportsDomainRedirects(selectedServer);
|
|
||||||
const asideClass = classNames('aside-menu', {
|
const asideClass = classNames('aside-menu', {
|
||||||
'aside-menu--hidden': !showOnMobile,
|
'aside-menu--hidden': !showOnMobile,
|
||||||
});
|
});
|
||||||
|
@ -68,12 +66,10 @@ export const AsideMenu = (DeleteServerButton: FC<DeleteServerButtonProps>) => (
|
||||||
<FontAwesomeIcon fixedWidth icon={tagsIcon} />
|
<FontAwesomeIcon fixedWidth icon={tagsIcon} />
|
||||||
<span className="aside-menu__item-text">Manage tags</span>
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
</AsideMenuItem>
|
</AsideMenuItem>
|
||||||
{addManageDomainsLink && (
|
<AsideMenuItem to={buildPath('/manage-domains')}>
|
||||||
<AsideMenuItem to={buildPath('/manage-domains')}>
|
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
|
||||||
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
|
<span className="aside-menu__item-text">Manage domains</span>
|
||||||
<span className="aside-menu__item-text">Manage domains</span>
|
</AsideMenuItem>
|
||||||
</AsideMenuItem>
|
|
||||||
)}
|
|
||||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||||
<FontAwesomeIcon fixedWidth icon={editIcon} />
|
<FontAwesomeIcon fixedWidth icon={editIcon} />
|
||||||
<span className="aside-menu__item-text">Edit this server</span>
|
<span className="aside-menu__item-text">Edit this server</span>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||||
import { supportsDomainRedirects, supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
|
import { supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import { NotFound } from './NotFound';
|
import { NotFound } from './NotFound';
|
||||||
import { AsideMenuProps } from './AsideMenu';
|
import { AsideMenuProps } from './AsideMenu';
|
||||||
|
@ -47,7 +47,6 @@ export const MenuLayout = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
|
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
|
||||||
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
|
|
||||||
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
|
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
|
||||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||||
|
@ -73,7 +72,7 @@ export const MenuLayout = (
|
||||||
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
|
<Route path="/orphan-visits/*" element={<OrphanVisits />} />
|
||||||
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
|
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
|
||||||
<Route path="/manage-tags" element={<TagsList />} />
|
<Route path="/manage-tags" element={<TagsList />} />
|
||||||
{addManageDomainsRoute && <Route path="/manage-domains" element={<ManageDomains />} />}
|
<Route path="/manage-domains" element={<ManageDomains />} />
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={<NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
element={<NotFound to={`/server/${selectedServer.id}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
import { cond, isEmpty, pipe, replace, trim, T } from 'ramda';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { DateTimeInput, DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
import { DateTimeInput, DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
||||||
import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features';
|
import { supportsForwardQuery } from '../utils/helpers/features';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
|
import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils';
|
||||||
import { Checkbox } from '../utils/Checkbox';
|
import { Checkbox } from '../utils/Checkbox';
|
||||||
|
@ -113,16 +113,14 @@ export const ShortUrlForm = (
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const showCrawlableControl = supportsCrawlableVisits(selectedServer);
|
|
||||||
const showForwardQueryControl = supportsForwardQuery(selectedServer);
|
const showForwardQueryControl = supportsForwardQuery(selectedServer);
|
||||||
const showBehaviorCard = showCrawlableControl || showForwardQueryControl;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
<form name="shortUrlForm" className="short-url-form" onSubmit={submit}>
|
||||||
{isBasicMode && basicComponents}
|
{isBasicMode && basicComponents}
|
||||||
{!isBasicMode && (
|
{!isBasicMode && (
|
||||||
<>
|
<>
|
||||||
<SimpleCard title="Basic options" className="mb-3">
|
<SimpleCard title="Main options" className="mb-3">
|
||||||
{basicComponents}
|
{basicComponents}
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
||||||
|
@ -190,30 +188,26 @@ export const ShortUrlForm = (
|
||||||
)}
|
)}
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
</div>
|
</div>
|
||||||
{showBehaviorCard && (
|
<div className="col-sm-6 mb-3">
|
||||||
<div className="col-sm-6 mb-3">
|
<SimpleCard title="Configure behavior">
|
||||||
<SimpleCard title="Configure behavior">
|
<ShortUrlFormCheckboxGroup
|
||||||
{showCrawlableControl && (
|
infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
|
||||||
<ShortUrlFormCheckboxGroup
|
checked={shortUrlData.crawlable}
|
||||||
infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
|
onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
|
||||||
checked={shortUrlData.crawlable}
|
>
|
||||||
onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
|
Make it crawlable
|
||||||
>
|
</ShortUrlFormCheckboxGroup>
|
||||||
Make it crawlable
|
{showForwardQueryControl && (
|
||||||
</ShortUrlFormCheckboxGroup>
|
<ShortUrlFormCheckboxGroup
|
||||||
)}
|
infoTooltip="When this short URL is visited, any query params appended to it will be forwarded to the long URL."
|
||||||
{showForwardQueryControl && (
|
checked={shortUrlData.forwardQuery}
|
||||||
<ShortUrlFormCheckboxGroup
|
onChange={(forwardQuery) => setShortUrlData({ ...shortUrlData, forwardQuery })}
|
||||||
infoTooltip="When this short URL is visited, any query params appended to it will be forwarded to the long URL."
|
>
|
||||||
checked={shortUrlData.forwardQuery}
|
Forward query params on redirect
|
||||||
onChange={(forwardQuery) => setShortUrlData({ ...shortUrlData, forwardQuery })}
|
</ShortUrlFormCheckboxGroup>
|
||||||
>
|
)}
|
||||||
Forward query params on redirect
|
</SimpleCard>
|
||||||
</ShortUrlFormCheckboxGroup>
|
</div>
|
||||||
)}
|
|
||||||
</SimpleCard>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { SearchField } from '../utils/SearchField';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
|
import { DateRange, datesToDateRange } from '../utils/helpers/dateIntervals';
|
||||||
import { supportsAllTagsFiltering, supportsBotVisits } from '../utils/helpers/features';
|
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { OrderDir } from '../utils/helpers/ordering';
|
import { OrderDir } from '../utils/helpers/ordering';
|
||||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||||
|
@ -47,7 +47,6 @@ export const ShortUrlsFilteringBar = (
|
||||||
);
|
);
|
||||||
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
const changeTagSelection = (selectedTags: string[]) => toFirstPage({ tags: selectedTags });
|
||||||
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
const canChangeTagsMode = supportsAllTagsFiltering(selectedServer);
|
||||||
const botsSupported = supportsBotVisits(selectedServer);
|
|
||||||
const toggleTagsMode = pipe(
|
const toggleTagsMode = pipe(
|
||||||
() => (tagsMode === 'any' ? 'all' : 'any'),
|
() => (tagsMode === 'any' ? 'all' : 'any'),
|
||||||
(mode) => toFirstPage({ tagsMode: mode }),
|
(mode) => toFirstPage({ tagsMode: mode }),
|
||||||
|
@ -83,7 +82,6 @@ export const ShortUrlsFilteringBar = (
|
||||||
</div>
|
</div>
|
||||||
<ShortUrlsFilterDropdown
|
<ShortUrlsFilterDropdown
|
||||||
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
||||||
botsSupported={botsSupported}
|
|
||||||
selected={{ excludeBots: excludeBots ?? settings.visits?.excludeBots }}
|
selected={{ excludeBots: excludeBots ?? settings.visits?.excludeBots }}
|
||||||
onChange={toFirstPage}
|
onChange={toFirstPage}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { ExternalLink } from 'react-external-link';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon';
|
||||||
import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCodes';
|
import { buildQrCodeUrl, QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCodes';
|
||||||
import { supportsNonRestCors, supportsQrErrorCorrection } from '../../utils/helpers/features';
|
import { supportsNonRestCors } from '../../utils/helpers/features';
|
||||||
import { ImageDownloader } from '../../common/services/ImageDownloader';
|
import { ImageDownloader } from '../../common/services/ImageDownloader';
|
||||||
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
|
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
|
||||||
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
|
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
|
||||||
|
@ -24,14 +24,10 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
const [margin, setMargin] = useState(0);
|
const [margin, setMargin] = useState(0);
|
||||||
const [format, setFormat] = useState<QrCodeFormat>('png');
|
const [format, setFormat] = useState<QrCodeFormat>('png');
|
||||||
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>('L');
|
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>('L');
|
||||||
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
|
||||||
errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer),
|
|
||||||
}), [selectedServer]);
|
|
||||||
const displayDownloadBtn = supportsNonRestCors(selectedServer);
|
const displayDownloadBtn = supportsNonRestCors(selectedServer);
|
||||||
const willRenderThreeControls = !capabilities.errorCorrectionIsSupported;
|
|
||||||
const qrCodeUrl = useMemo(
|
const qrCodeUrl = useMemo(
|
||||||
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities),
|
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }),
|
||||||
[shortUrl, size, format, margin, errorCorrection, capabilities],
|
[shortUrl, size, format, margin, errorCorrection],
|
||||||
);
|
);
|
||||||
const totalSize = useMemo(() => size + margin, [size, margin]);
|
const totalSize = useMemo(() => size + margin, [size, margin]);
|
||||||
const modalSize = useMemo(() => {
|
const modalSize = useMemo(() => {
|
||||||
|
@ -49,7 +45,7 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
<FormGroup className="d-grid col-md-4">
|
||||||
<label>Size: {size}px</label>
|
<label>Size: {size}px</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
|
@ -61,7 +57,7 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
onChange={(e) => setSize(Number(e.target.value))}
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className={`d-grid ${willRenderThreeControls ? 'col-md-4' : 'col-md-6'}`}>
|
<FormGroup className="d-grid col-md-4">
|
||||||
<label htmlFor="marginControl">Margin: {margin}px</label>
|
<label htmlFor="marginControl">Margin: {margin}px</label>
|
||||||
<input
|
<input
|
||||||
id="marginControl"
|
id="marginControl"
|
||||||
|
@ -74,14 +70,12 @@ export const QrCodeModal = (imageDownloader: ImageDownloader) => (
|
||||||
onChange={(e) => setMargin(Number(e.target.value))}
|
onChange={(e) => setMargin(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup className={willRenderThreeControls ? 'col-md-4' : 'col-md-6'}>
|
<FormGroup className="d-grid col-md-4">
|
||||||
<QrFormatDropdown format={format} setFormat={setFormat} />
|
<QrFormatDropdown format={format} setFormat={setFormat} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
{capabilities.errorCorrectionIsSupported && (
|
<FormGroup className="col-md-6">
|
||||||
<FormGroup className="col-md-6">
|
<QrErrorCorrectionDropdown errorCorrection={errorCorrection} setErrorCorrection={setErrorCorrection} />
|
||||||
<QrErrorCorrectionDropdown errorCorrection={errorCorrection} setErrorCorrection={setErrorCorrection} />
|
</FormGroup>
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|
|
@ -7,27 +7,18 @@ interface ShortUrlsFilterDropdownProps {
|
||||||
onChange: (filters: ShortUrlsFilter) => void;
|
onChange: (filters: ShortUrlsFilter) => void;
|
||||||
selected?: ShortUrlsFilter;
|
selected?: ShortUrlsFilter;
|
||||||
className?: string;
|
className?: string;
|
||||||
botsSupported: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShortUrlsFilterDropdown = (
|
export const ShortUrlsFilterDropdown = (
|
||||||
{ onChange, selected = {}, className, botsSupported }: ShortUrlsFilterDropdownProps,
|
{ onChange, selected = {}, className }: ShortUrlsFilterDropdownProps,
|
||||||
) => {
|
) => {
|
||||||
if (!botsSupported) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { excludeBots = false } = selected;
|
const { excludeBots = false } = selected;
|
||||||
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
||||||
{botsSupported && (
|
<DropdownItem header>Bots:</DropdownItem>
|
||||||
<>
|
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude bots visits</DropdownItem>
|
||||||
<DropdownItem header>Bots:</DropdownItem>
|
|
||||||
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude bots visits</DropdownItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem disabled={!hasValue(selected)} onClick={() => onChange({ excludeBots: false })}>
|
<DropdownItem disabled={!hasValue(selected)} onClick={() => onChange({ excludeBots: false })}>
|
||||||
|
|
|
@ -4,10 +4,6 @@ import { SemVerPattern, versionMatch } from './version';
|
||||||
const serverMatchesMinVersion = (minVersion: SemVerPattern) => (selectedServer: SelectedServer): boolean =>
|
const serverMatchesMinVersion = (minVersion: SemVerPattern) => (selectedServer: SelectedServer): boolean =>
|
||||||
isReachableServer(selectedServer) && versionMatch(selectedServer.version, { minVersion });
|
isReachableServer(selectedServer) && versionMatch(selectedServer.version, { minVersion });
|
||||||
|
|
||||||
export const supportsBotVisits = serverMatchesMinVersion('2.7.0');
|
|
||||||
export const supportsCrawlableVisits = supportsBotVisits;
|
|
||||||
export const supportsQrErrorCorrection = serverMatchesMinVersion('2.8.0');
|
|
||||||
export const supportsDomainRedirects = supportsQrErrorCorrection;
|
|
||||||
export const supportsForwardQuery = serverMatchesMinVersion('2.9.0');
|
export const supportsForwardQuery = serverMatchesMinVersion('2.9.0');
|
||||||
export const supportsNonRestCors = supportsForwardQuery;
|
export const supportsNonRestCors = supportsForwardQuery;
|
||||||
export const supportsDefaultDomainRedirectsEdition = serverMatchesMinVersion('2.10.0');
|
export const supportsDefaultDomainRedirectsEdition = serverMatchesMinVersion('2.10.0');
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { isEmpty } from 'ramda';
|
import { isEmpty } from 'ramda';
|
||||||
import { stringifyQuery } from './query';
|
import { stringifyQuery } from './query';
|
||||||
|
|
||||||
export interface QrCodeCapabilities {
|
|
||||||
errorCorrectionIsSupported: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QrCodeFormat = 'svg' | 'png';
|
export type QrCodeFormat = 'svg' | 'png';
|
||||||
|
|
||||||
export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
|
||||||
|
@ -16,17 +12,11 @@ export interface QrCodeOptions {
|
||||||
errorCorrection: QrErrorCorrection;
|
errorCorrection: QrErrorCorrection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildQrCodeUrl = (
|
export const buildQrCodeUrl = (shortUrl: string, { margin, ...options }: QrCodeOptions): string => {
|
||||||
shortUrl: string,
|
|
||||||
{ size, format, margin, errorCorrection }: QrCodeOptions,
|
|
||||||
{ errorCorrectionIsSupported }: QrCodeCapabilities,
|
|
||||||
): string => {
|
|
||||||
const baseUrl = `${shortUrl}/qr-code`;
|
const baseUrl = `${shortUrl}/qr-code`;
|
||||||
const query = stringifyQuery({
|
const query = stringifyQuery({
|
||||||
size,
|
...options,
|
||||||
format,
|
|
||||||
margin: margin > 0 ? margin : undefined,
|
margin: margin > 0 ? margin : undefined,
|
||||||
errorCorrection: errorCorrectionIsSupported ? errorCorrection : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`;
|
return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`;
|
||||||
|
|
|
@ -22,7 +22,6 @@ export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
||||||
domainVisits,
|
domainVisits,
|
||||||
cancelGetDomainVisits,
|
cancelGetDomainVisits,
|
||||||
settings,
|
settings,
|
||||||
selectedServer,
|
|
||||||
}: DomainVisitsProps) => {
|
}: DomainVisitsProps) => {
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const { domain = '' } = useParams();
|
const { domain = '' } = useParams();
|
||||||
|
@ -38,7 +37,6 @@ export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
||||||
visitsInfo={domainVisits}
|
visitsInfo={domainVisits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
|
||||||
>
|
>
|
||||||
<VisitsHeader goBack={goBack} visits={domainVisits.visits} title={`"${authority}" visits`} />
|
<VisitsHeader goBack={goBack} visits={domainVisits.visits} title={`"${authority}" visits`} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -20,7 +20,6 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc
|
||||||
nonOrphanVisits,
|
nonOrphanVisits,
|
||||||
cancelGetNonOrphanVisits,
|
cancelGetNonOrphanVisits,
|
||||||
settings,
|
settings,
|
||||||
selectedServer,
|
|
||||||
}: NonOrphanVisitsProps) => {
|
}: NonOrphanVisitsProps) => {
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits);
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits);
|
||||||
|
@ -34,7 +33,6 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc
|
||||||
visitsInfo={nonOrphanVisits}
|
visitsInfo={nonOrphanVisits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
|
||||||
>
|
>
|
||||||
<VisitsHeader title="Non-orphan visits" goBack={goBack} visits={nonOrphanVisits.visits} />
|
<VisitsHeader title="Non-orphan visits" goBack={goBack} visits={nonOrphanVisits.visits} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -21,7 +21,6 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
||||||
orphanVisits,
|
orphanVisits,
|
||||||
cancelGetOrphanVisits,
|
cancelGetOrphanVisits,
|
||||||
settings,
|
settings,
|
||||||
selectedServer,
|
|
||||||
}: OrphanVisitsProps) => {
|
}: OrphanVisitsProps) => {
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
||||||
|
@ -36,7 +35,6 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
||||||
visitsInfo={orphanVisits}
|
visitsInfo={orphanVisits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
|
||||||
isOrphanVisits
|
isOrphanVisits
|
||||||
>
|
>
|
||||||
<VisitsHeader title="Orphan visits" goBack={goBack} visits={orphanVisits.visits} />
|
<VisitsHeader title="Orphan visits" goBack={goBack} visits={orphanVisits.visits} />
|
||||||
|
|
|
@ -30,7 +30,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
|
||||||
getShortUrlDetail,
|
getShortUrlDetail,
|
||||||
cancelGetShortUrlVisits,
|
cancelGetShortUrlVisits,
|
||||||
settings,
|
settings,
|
||||||
selectedServer,
|
|
||||||
}: ShortUrlVisitsProps) => {
|
}: ShortUrlVisitsProps) => {
|
||||||
const { shortCode = '' } = useParams<{ shortCode: string }>();
|
const { shortCode = '' } = useParams<{ shortCode: string }>();
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
@ -57,7 +56,6 @@ export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercu
|
||||||
visitsInfo={shortUrlVisits}
|
visitsInfo={shortUrlVisits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
|
||||||
>
|
>
|
||||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -23,7 +23,6 @@ export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: Repo
|
||||||
tagVisits,
|
tagVisits,
|
||||||
cancelGetTagVisits,
|
cancelGetTagVisits,
|
||||||
settings,
|
settings,
|
||||||
selectedServer,
|
|
||||||
}: TagVisitsProps) => {
|
}: TagVisitsProps) => {
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
const { tag = '' } = useParams();
|
const { tag = '' } = useParams();
|
||||||
|
@ -38,7 +37,6 @@ export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: Repo
|
||||||
visitsInfo={tagVisits}
|
visitsInfo={tagVisits}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={selectedServer}
|
|
||||||
>
|
>
|
||||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||||
</VisitsStats>
|
</VisitsStats>
|
||||||
|
|
|
@ -11,8 +11,6 @@ import { Message } from '../utils/Message';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
import { Settings } from '../settings/reducers/settings';
|
import { Settings } from '../settings/reducers/settings';
|
||||||
import { SelectedServer } from '../servers/data';
|
|
||||||
import { supportsBotVisits } from '../utils/helpers/features';
|
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { NavPillItem, NavPills } from '../utils/NavPills';
|
import { NavPillItem, NavPills } from '../utils/NavPills';
|
||||||
import { ExportBtn } from '../utils/ExportBtn';
|
import { ExportBtn } from '../utils/ExportBtn';
|
||||||
|
@ -33,7 +31,6 @@ export type VisitsStatsProps = PropsWithChildren<{
|
||||||
getVisits: (params: VisitsParams, doIntervalFallback?: boolean) => void;
|
getVisits: (params: VisitsParams, doIntervalFallback?: boolean) => void;
|
||||||
visitsInfo: VisitsInfo;
|
visitsInfo: VisitsInfo;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
cancelGetVisits: () => void;
|
cancelGetVisits: () => void;
|
||||||
exportCsv: (visits: NormalizedVisit[]) => void;
|
exportCsv: (visits: NormalizedVisit[]) => void;
|
||||||
isOrphanVisits?: boolean;
|
isOrphanVisits?: boolean;
|
||||||
|
@ -63,7 +60,6 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
cancelGetVisits,
|
cancelGetVisits,
|
||||||
settings,
|
settings,
|
||||||
exportCsv,
|
exportCsv,
|
||||||
selectedServer,
|
|
||||||
isOrphanVisits = false,
|
isOrphanVisits = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { visits, loading, loadingLarge, error, errorData, progress, fallbackInterval } = visitsInfo;
|
const { visits, loading, loadingLarge, error, errorData, progress, fallbackInterval } = visitsInfo;
|
||||||
|
@ -82,7 +78,6 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
);
|
);
|
||||||
const [highlightedVisits, setHighlightedVisits] = useState<NormalizedVisit[]>([]);
|
const [highlightedVisits, setHighlightedVisits] = useState<NormalizedVisit[]>([]);
|
||||||
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
|
const [highlightedLabel, setHighlightedLabel] = useState<string | undefined>();
|
||||||
const botsSupported = supportsBotVisits(selectedServer);
|
|
||||||
const isFirstLoad = useRef(true);
|
const isFirstLoad = useRef(true);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
@ -273,7 +268,6 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
selectedVisits={highlightedVisits}
|
selectedVisits={highlightedVisits}
|
||||||
setSelectedVisits={setSelectedVisits}
|
setSelectedVisits={setSelectedVisits}
|
||||||
isOrphanVisits={isOrphanVisits}
|
isOrphanVisits={isOrphanVisits}
|
||||||
selectedServer={selectedServer}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -306,7 +300,6 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
<VisitsFilterDropdown
|
<VisitsFilterDropdown
|
||||||
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
className="ms-0 ms-md-2 mt-3 mt-md-0"
|
||||||
isOrphanVisits={isOrphanVisits}
|
isOrphanVisits={isOrphanVisits}
|
||||||
botsSupported={botsSupported}
|
|
||||||
selected={resolvedFilter}
|
selected={resolvedFilter}
|
||||||
onChange={(newVisitsFilter) => updateFiltering({ visitsFilter: newVisitsFilter })}
|
onChange={(newVisitsFilter) => updateFiltering({ visitsFilter: newVisitsFilter })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { SimplePaginator } from '../common/SimplePaginator';
|
||||||
import { SearchField } from '../utils/SearchField';
|
import { SearchField } from '../utils/SearchField';
|
||||||
import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering';
|
import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { supportsBotVisits } from '../utils/helpers/features';
|
|
||||||
import { SelectedServer } from '../servers/data';
|
|
||||||
import { Time } from '../utils/dates/Time';
|
import { Time } from '../utils/dates/Time';
|
||||||
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
|
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
|
||||||
import { MediaMatcher } from '../utils/types';
|
import { MediaMatcher } from '../utils/types';
|
||||||
|
@ -22,7 +20,6 @@ export interface VisitsTableProps {
|
||||||
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
||||||
matchMedia?: MediaMatcher;
|
matchMedia?: MediaMatcher;
|
||||||
isOrphanVisits?: boolean;
|
isOrphanVisits?: boolean;
|
||||||
selectedServer: SelectedServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot';
|
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot';
|
||||||
|
@ -49,7 +46,6 @@ export const VisitsTable = ({
|
||||||
visits,
|
visits,
|
||||||
selectedVisits = [],
|
selectedVisits = [],
|
||||||
setSelectedVisits,
|
setSelectedVisits,
|
||||||
selectedServer,
|
|
||||||
matchMedia = window.matchMedia,
|
matchMedia = window.matchMedia,
|
||||||
isOrphanVisits = false,
|
isOrphanVisits = false,
|
||||||
}: VisitsTableProps) => {
|
}: VisitsTableProps) => {
|
||||||
|
@ -64,8 +60,7 @@ export const VisitsTable = ({
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const end = page * PAGE_SIZE;
|
const end = page * PAGE_SIZE;
|
||||||
const start = end - PAGE_SIZE;
|
const start = end - PAGE_SIZE;
|
||||||
const supportsBots = supportsBotVisits(selectedServer);
|
const fullSizeColSpan = 8 + Number(isOrphanVisits);
|
||||||
const fullSizeColSpan = 7 + Number(supportsBots) + Number(isOrphanVisits);
|
|
||||||
|
|
||||||
const orderByColumn = (field: OrderableFields) =>
|
const orderByColumn = (field: OrderableFields) =>
|
||||||
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
||||||
|
@ -99,12 +94,10 @@ export const VisitsTable = ({
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
||||||
</th>
|
</th>
|
||||||
{supportsBots && (
|
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
||||||
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
<FontAwesomeIcon icon={botIcon} />
|
||||||
<FontAwesomeIcon icon={botIcon} />
|
{renderOrderIcon('potentialBot')}
|
||||||
{renderOrderIcon('potentialBot')}
|
</th>
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
||||||
Date
|
Date
|
||||||
{renderOrderIcon('date')}
|
{renderOrderIcon('date')}
|
||||||
|
@ -165,18 +158,16 @@ export const VisitsTable = ({
|
||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
||||||
</td>
|
</td>
|
||||||
{supportsBots && (
|
<td className="text-center">
|
||||||
<td className="text-center">
|
{visit.potentialBot && (
|
||||||
{visit.potentialBot && (
|
<>
|
||||||
<>
|
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
||||||
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
||||||
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
Potentially a visit from a bot or crawler
|
||||||
Potentially a visit from a bot or crawler
|
</UncontrolledTooltip>
|
||||||
</UncontrolledTooltip>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</td>
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td><Time date={visit.date} /></td>
|
<td><Time date={visit.date} /></td>
|
||||||
<td>{visit.country}</td>
|
<td>{visit.country}</td>
|
||||||
<td>{visit.city}</td>
|
<td>{visit.city}</td>
|
||||||
|
|
|
@ -8,16 +8,11 @@ interface VisitsFilterDropdownProps {
|
||||||
selected?: VisitsFilter;
|
selected?: VisitsFilter;
|
||||||
className?: string;
|
className?: string;
|
||||||
isOrphanVisits: boolean;
|
isOrphanVisits: boolean;
|
||||||
botsSupported: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VisitsFilterDropdown = (
|
export const VisitsFilterDropdown = (
|
||||||
{ onChange, selected = {}, className, isOrphanVisits, botsSupported }: VisitsFilterDropdownProps,
|
{ onChange, selected = {}, className, isOrphanVisits }: VisitsFilterDropdownProps,
|
||||||
) => {
|
) => {
|
||||||
if (!botsSupported && !isOrphanVisits) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orphanVisitsType, excludeBots = false } = selected;
|
const { orphanVisitsType, excludeBots = false } = selected;
|
||||||
const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({
|
const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({
|
||||||
active: orphanVisitsType === type,
|
active: orphanVisitsType === type,
|
||||||
|
@ -27,17 +22,12 @@ export const VisitsFilterDropdown = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}>
|
||||||
{botsSupported && (
|
<DropdownItem header>Bots:</DropdownItem>
|
||||||
<>
|
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude potential bots</DropdownItem>
|
||||||
<DropdownItem header>Bots:</DropdownItem>
|
|
||||||
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude potential bots</DropdownItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{botsSupported && isOrphanVisits && <DropdownItem divider />}
|
|
||||||
|
|
||||||
{isOrphanVisits && (
|
{isOrphanVisits && (
|
||||||
<>
|
<>
|
||||||
|
<DropdownItem divider />
|
||||||
<DropdownItem header>Orphan visits type:</DropdownItem>
|
<DropdownItem header>Orphan visits type:</DropdownItem>
|
||||||
<DropdownItem {...propsForOrphanVisitsTypeItem('base_url')}>Base URL</DropdownItem>
|
<DropdownItem {...propsForOrphanVisitsTypeItem('base_url')}>Base URL</DropdownItem>
|
||||||
<DropdownItem {...propsForOrphanVisitsTypeItem('invalid_short_url')}>Invalid short URL</DropdownItem>
|
<DropdownItem {...propsForOrphanVisitsTypeItem('invalid_short_url')}>Invalid short URL</DropdownItem>
|
||||||
|
|
|
@ -82,7 +82,7 @@ export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.redu
|
||||||
);
|
);
|
||||||
|
|
||||||
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
||||||
const { userAgent, date, referer, visitLocation, potentialBot = false } = visit;
|
const { userAgent, date, referer, visitLocation, potentialBot } = visit;
|
||||||
const common = {
|
const common = {
|
||||||
date,
|
date,
|
||||||
potentialBot,
|
potentialBot,
|
||||||
|
|
|
@ -22,31 +22,31 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter');
|
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter');
|
||||||
bottle.decorator('ShortUrlVisits', connect(
|
bottle.decorator('ShortUrlVisits', connect(
|
||||||
['shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer'],
|
['shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings'],
|
||||||
['getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter');
|
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter');
|
||||||
bottle.decorator('TagVisits', connect(
|
bottle.decorator('TagVisits', connect(
|
||||||
['tagVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
['tagVisits', 'mercureInfo', 'settings'],
|
||||||
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('DomainVisits', DomainVisits, 'ReportExporter');
|
bottle.serviceFactory('DomainVisits', DomainVisits, 'ReportExporter');
|
||||||
bottle.decorator('DomainVisits', connect(
|
bottle.decorator('DomainVisits', connect(
|
||||||
['domainVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
['domainVisits', 'mercureInfo', 'settings'],
|
||||||
['getDomainVisits', 'cancelGetDomainVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getDomainVisits', 'cancelGetDomainVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
||||||
bottle.decorator('OrphanVisits', connect(
|
bottle.decorator('OrphanVisits', connect(
|
||||||
['orphanVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
['orphanVisits', 'mercureInfo', 'settings'],
|
||||||
['getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter');
|
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter');
|
||||||
bottle.decorator('NonOrphanVisits', connect(
|
bottle.decorator('NonOrphanVisits', connect(
|
||||||
['nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
['nonOrphanVisits', 'mercureInfo', 'settings'],
|
||||||
['getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { SelectedServer } from '../../servers/data';
|
|
||||||
import { Settings } from '../../settings/reducers/settings';
|
import { Settings } from '../../settings/reducers/settings';
|
||||||
|
|
||||||
export interface CommonVisitsProps {
|
export interface CommonVisitsProps {
|
||||||
selectedServer: SelectedServer;
|
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface RegularVisit {
|
||||||
date: string;
|
date: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
visitLocation: VisitLocation | null;
|
visitLocation: VisitLocation | null;
|
||||||
potentialBot?: boolean; // Optional only when using Shlink older than v2.7
|
potentialBot: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrphanVisit extends RegularVisit {
|
export interface OrphanVisit extends RegularVisit {
|
||||||
|
|
|
@ -3,26 +3,22 @@ import { Mock } from 'ts-mockery';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu';
|
import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu';
|
||||||
import { ReachableServer } from '../../src/servers/data';
|
import { ReachableServer } from '../../src/servers/data';
|
||||||
import { SemVer } from '../../src/utils/helpers/version';
|
|
||||||
|
|
||||||
describe('<AsideMenu />', () => {
|
describe('<AsideMenu />', () => {
|
||||||
const AsideMenu = createAsideMenu(() => <>DeleteServerButton</>);
|
const AsideMenu = createAsideMenu(() => <>DeleteServerButton</>);
|
||||||
const setUp = (version: SemVer, id: string | false = 'abc123') => render(
|
const setUp = (id: string | false = 'abc123') => render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: id || undefined, version })} />
|
<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: id || undefined, version: '2.8.0' })} />
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each([
|
it('contains links to different sections', () => {
|
||||||
['2.7.0' as SemVer, 5],
|
setUp();
|
||||||
['2.8.0' as SemVer, 6],
|
|
||||||
])('contains links to different sections', (version, expectedAmountOfLinks) => {
|
|
||||||
setUp(version);
|
|
||||||
|
|
||||||
const links = screen.getAllByRole('link');
|
const links = screen.getAllByRole('link');
|
||||||
|
|
||||||
expect.assertions(links.length + 1);
|
expect.assertions(links.length + 1);
|
||||||
expect(links).toHaveLength(expectedAmountOfLinks);
|
expect(links).toHaveLength(6);
|
||||||
links.forEach((link) => expect(link.getAttribute('href')).toContain('abc123'));
|
links.forEach((link) => expect(link.getAttribute('href')).toContain('abc123'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,7 +26,7 @@ describe('<AsideMenu />', () => {
|
||||||
['abc', true],
|
['abc', true],
|
||||||
[false, false],
|
[false, false],
|
||||||
])('contains a button to delete server if appropriate', (id, shouldHaveBtn) => {
|
])('contains a button to delete server if appropriate', (id, shouldHaveBtn) => {
|
||||||
setUp('2.8.0', id as string | false);
|
setUp(id as string | false);
|
||||||
|
|
||||||
if (shouldHaveBtn) {
|
if (shouldHaveBtn) {
|
||||||
expect(screen.getByText('DeleteServerButton')).toBeInTheDocument();
|
expect(screen.getByText('DeleteServerButton')).toBeInTheDocument();
|
||||||
|
|
|
@ -77,7 +77,6 @@ describe('<MenuLayout />', () => {
|
||||||
['3.1.0' as SemVer, '/domain/domain.com/visits/foo', 'DomainVisits'],
|
['3.1.0' as SemVer, '/domain/domain.com/visits/foo', 'DomainVisits'],
|
||||||
['2.10.0' as SemVer, '/non-orphan-visits/foo', 'Oops! We could not find requested route.'],
|
['2.10.0' as SemVer, '/non-orphan-visits/foo', 'Oops! We could not find requested route.'],
|
||||||
['3.0.0' as SemVer, '/non-orphan-visits/foo', 'NonOrphanVisits'],
|
['3.0.0' as SemVer, '/non-orphan-visits/foo', 'NonOrphanVisits'],
|
||||||
['2.7.0' as SemVer, '/manage-domains', 'Oops! We could not find requested route.'],
|
|
||||||
['2.8.0' as SemVer, '/manage-domains', 'ManageDomains'],
|
['2.8.0' as SemVer, '/manage-domains', 'ManageDomains'],
|
||||||
])(
|
])(
|
||||||
'renders expected component based on location and server version',
|
'renders expected component based on location and server version',
|
||||||
|
|
|
@ -64,7 +64,7 @@ describe('<ShortUrlForm />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['create' as Mode, 4],
|
['create' as Mode, 5],
|
||||||
['create-basic' as Mode, 0],
|
['create-basic' as Mode, 0],
|
||||||
])(
|
])(
|
||||||
'renders expected amount of cards based on server capabilities and mode',
|
'renders expected amount of cards based on server capabilities and mode',
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe('<QrCodeModal />', () => {
|
||||||
const saveImage = jest.fn().mockReturnValue(Promise.resolve());
|
const saveImage = jest.fn().mockReturnValue(Promise.resolve());
|
||||||
const QrCodeModal = createQrCodeModal(Mock.of<ImageDownloader>({ saveImage }));
|
const QrCodeModal = createQrCodeModal(Mock.of<ImageDownloader>({ saveImage }));
|
||||||
const shortUrl = 'https://doma.in/abc123';
|
const shortUrl = 'https://doma.in/abc123';
|
||||||
const setUp = (version: SemVer = '2.6.0') => renderWithEvents(
|
const setUp = (version: SemVer = '2.8.0') => renderWithEvents(
|
||||||
<QrCodeModal
|
<QrCodeModal
|
||||||
isOpen
|
isOpen
|
||||||
shortUrl={Mock.of<ShortUrl>({ shortUrl })}
|
shortUrl={Mock.of<ShortUrl>({ shortUrl })}
|
||||||
|
@ -32,12 +32,10 @@ describe('<QrCodeModal />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['2.5.0' as SemVer, 0, '/qr-code?size=300&format=png'],
|
[10, '/qr-code?size=300&format=png&errorCorrection=L&margin=10'],
|
||||||
['2.6.0' as SemVer, 0, '/qr-code?size=300&format=png'],
|
[0, '/qr-code?size=300&format=png&errorCorrection=L'],
|
||||||
['2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10'],
|
])('displays an image with the QR code of the URL', async (margin, expectedUrl) => {
|
||||||
['2.8.0' as SemVer, 0, '/qr-code?size=300&format=png&errorCorrection=L'],
|
const { container } = setUp();
|
||||||
])('displays an image with the QR code of the URL', async (version, margin, expectedUrl) => {
|
|
||||||
const { container } = setUp(version);
|
|
||||||
const marginControl = container.parentNode?.querySelectorAll('.form-control-range').item(1);
|
const marginControl = container.parentNode?.querySelectorAll('.form-control-range').item(1);
|
||||||
|
|
||||||
if (marginControl) {
|
if (marginControl) {
|
||||||
|
@ -69,16 +67,13 @@ describe('<QrCodeModal />', () => {
|
||||||
modalSize && expect(screen.getByRole('document')).toHaveClass(`modal-${modalSize}`);
|
modalSize && expect(screen.getByRole('document')).toHaveClass(`modal-${modalSize}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('shows expected components based on server version', () => {
|
||||||
['2.6.0' as SemVer, 1, 'col-md-4'],
|
const { container } = setUp();
|
||||||
['2.8.0' as SemVer, 2, 'col-md-6'],
|
|
||||||
])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => {
|
|
||||||
const { container } = setUp(version);
|
|
||||||
const dropdowns = screen.getAllByRole('button');
|
const dropdowns = screen.getAllByRole('button');
|
||||||
const firstCol = container.parentNode?.querySelectorAll('.d-grid').item(0);
|
const firstCol = container.parentNode?.querySelectorAll('.d-grid').item(0);
|
||||||
|
|
||||||
expect(dropdowns).toHaveLength(expectedAmountOfDropdowns + 1); // Add one because of the close button
|
expect(dropdowns).toHaveLength(2 + 1); // Add one because of the close button
|
||||||
expect(firstCol).toHaveClass(expectedRangeClass);
|
expect(firstCol).toHaveClass('col-md-4');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('saves the QR code image when clicking the Download button', async () => {
|
it('saves the QR code image when clicking the Download button', async () => {
|
||||||
|
|
|
@ -6,41 +6,30 @@ describe('qrCodes', () => {
|
||||||
[
|
[
|
||||||
'bar.io',
|
'bar.io',
|
||||||
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ errorCorrectionIsSupported: false },
|
'bar.io/qr-code?size=870&format=svg&errorCorrection=L',
|
||||||
'bar.io/qr-code?size=870&format=svg',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'bar.io',
|
|
||||||
{ size: 200, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
|
||||||
{ errorCorrectionIsSupported: false },
|
|
||||||
'bar.io/qr-code?size=200&format=png',
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'bar.io',
|
'bar.io',
|
||||||
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ errorCorrectionIsSupported: false },
|
'bar.io/qr-code?size=200&format=svg&errorCorrection=L',
|
||||||
'bar.io/qr-code?size=200&format=svg',
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
|
{ size: 456, format: 'png' as QrCodeFormat, margin: 10, errorCorrection: 'L' as QrErrorCorrection },
|
||||||
{ errorCorrectionIsSupported: false },
|
'shlink.io/qr-code?size=456&format=png&errorCorrection=L&margin=10',
|
||||||
'shlink.io/qr-code?size=456&format=png&margin=10',
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'H' as QrErrorCorrection },
|
{ size: 456, format: 'png' as QrCodeFormat, margin: 0, errorCorrection: 'H' as QrErrorCorrection },
|
||||||
{ errorCorrectionIsSupported: true },
|
|
||||||
'shlink.io/qr-code?size=456&format=png&errorCorrection=H',
|
'shlink.io/qr-code?size=456&format=png&errorCorrection=H',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'shlink.io',
|
'shlink.io',
|
||||||
{ size: 999, format: 'png' as QrCodeFormat, margin: 20, errorCorrection: 'Q' as QrErrorCorrection },
|
{ size: 999, format: 'png' as QrCodeFormat, margin: 20, errorCorrection: 'Q' as QrErrorCorrection },
|
||||||
{ errorCorrectionIsSupported: true },
|
'shlink.io/qr-code?size=999&format=png&errorCorrection=Q&margin=20',
|
||||||
'shlink.io/qr-code?size=999&format=png&margin=20&errorCorrection=Q',
|
|
||||||
],
|
],
|
||||||
])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => {
|
])('builds expected URL based in params', (shortUrl, options, expectedUrl) => {
|
||||||
expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl);
|
expect(buildQrCodeUrl(shortUrl, options)).toEqual(expectedUrl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||||
import { DomainVisits } from '../../src/visits/reducers/domainVisits';
|
import { DomainVisits } from '../../src/visits/reducers/domainVisits';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { SelectedServer } from '../../src/servers/data';
|
|
||||||
import { Visit } from '../../src/visits/types';
|
import { Visit } from '../../src/visits/types';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
|
@ -30,7 +29,6 @@ describe('<DomainVisits />', () => {
|
||||||
cancelGetDomainVisits={cancelGetDomainVisits}
|
cancelGetDomainVisits={cancelGetDomainVisits}
|
||||||
domainVisits={domainVisits}
|
domainVisits={domainVisits}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
selectedServer={Mock.all<SelectedServer>()}
|
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||||
import { Visit } from '../../src/visits/types';
|
import { Visit } from '../../src/visits/types';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||||
import { SelectedServer } from '../../src/servers/data';
|
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
import { VisitsInfo } from '../../src/visits/reducers/types';
|
import { VisitsInfo } from '../../src/visits/reducers/types';
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ describe('<NonOrphanVisits />', () => {
|
||||||
cancelGetNonOrphanVisits={cancelGetNonOrphanVisits}
|
cancelGetNonOrphanVisits={cancelGetNonOrphanVisits}
|
||||||
nonOrphanVisits={nonOrphanVisits}
|
nonOrphanVisits={nonOrphanVisits}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
selectedServer={Mock.all<SelectedServer>()}
|
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||||
import { Visit } from '../../src/visits/types';
|
import { Visit } from '../../src/visits/types';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||||
import { SelectedServer } from '../../src/servers/data';
|
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
import { VisitsInfo } from '../../src/visits/reducers/types';
|
import { VisitsInfo } from '../../src/visits/reducers/types';
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ describe('<OrphanVisits />', () => {
|
||||||
orphanVisits={orphanVisits}
|
orphanVisits={orphanVisits}
|
||||||
cancelGetOrphanVisits={jest.fn()}
|
cancelGetOrphanVisits={jest.fn()}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
selectedServer={Mock.all<SelectedServer>()}
|
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { createMemoryHistory } from 'history';
|
||||||
import { VisitsStats } from '../../src/visits/VisitsStats';
|
import { VisitsStats } from '../../src/visits/VisitsStats';
|
||||||
import { Visit } from '../../src/visits/types';
|
import { Visit } from '../../src/visits/types';
|
||||||
import { Settings } from '../../src/settings/reducers/settings';
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
import { ReachableServer } from '../../src/servers/data';
|
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
import { rangeOf } from '../../src/utils/utils';
|
import { rangeOf } from '../../src/utils/utils';
|
||||||
import { VisitsInfo } from '../../src/visits/reducers/types';
|
import { VisitsInfo } from '../../src/visits/reducers/types';
|
||||||
|
@ -28,7 +27,6 @@ describe('<VisitsStats />', () => {
|
||||||
cancelGetVisits={() => {}}
|
cancelGetVisits={() => {}}
|
||||||
settings={Mock.all<Settings>()}
|
settings={Mock.all<Settings>()}
|
||||||
exportCsv={exportCsv}
|
exportCsv={exportCsv}
|
||||||
selectedServer={Mock.of<ReachableServer>({ version: '3.0.0' })}
|
|
||||||
/>
|
/>
|
||||||
</Router>,
|
</Router>,
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { Mock } from 'ts-mockery';
|
||||||
import { VisitsTable, VisitsTableProps } from '../../src/visits/VisitsTable';
|
import { VisitsTable, VisitsTableProps } from '../../src/visits/VisitsTable';
|
||||||
import { rangeOf } from '../../src/utils/utils';
|
import { rangeOf } from '../../src/utils/utils';
|
||||||
import { NormalizedVisit } from '../../src/visits/types';
|
import { NormalizedVisit } from '../../src/visits/types';
|
||||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
|
||||||
import { SemVer } from '../../src/utils/helpers/version';
|
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<VisitsTable />', () => {
|
describe('<VisitsTable />', () => {
|
||||||
|
@ -13,7 +11,6 @@ describe('<VisitsTable />', () => {
|
||||||
const setUpFactory = (props: Partial<VisitsTableProps> = {}) => renderWithEvents(
|
const setUpFactory = (props: Partial<VisitsTableProps> = {}) => renderWithEvents(
|
||||||
<VisitsTable
|
<VisitsTable
|
||||||
visits={[]}
|
visits={[]}
|
||||||
selectedServer={Mock.all<SelectedServer>()}
|
|
||||||
{...props}
|
{...props}
|
||||||
matchMedia={matchMedia}
|
matchMedia={matchMedia}
|
||||||
setSelectedVisits={setSelectedVisits}
|
setSelectedVisits={setSelectedVisits}
|
||||||
|
@ -22,15 +19,8 @@ describe('<VisitsTable />', () => {
|
||||||
const setUp = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => setUpFactory(
|
const setUp = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => setUpFactory(
|
||||||
{ visits, selectedVisits },
|
{ visits, selectedVisits },
|
||||||
);
|
);
|
||||||
const setUpForOrphanVisits = (isOrphanVisits: boolean, version: SemVer) => setUpFactory({
|
const setUpForOrphanVisits = (isOrphanVisits: boolean) => setUpFactory({ isOrphanVisits });
|
||||||
isOrphanVisits,
|
|
||||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
|
||||||
});
|
|
||||||
const setUpForServerVersion = (version: SemVer) => setUpFactory({
|
|
||||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
|
||||||
});
|
|
||||||
const setUpWithBots = () => setUpFactory({
|
const setUpWithBots = () => setUpFactory({
|
||||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: '2.7.0', version: '2.7.0' }),
|
|
||||||
visits: [
|
visits: [
|
||||||
Mock.of<NormalizedVisit>({ potentialBot: false, date: '2022-05-05' }),
|
Mock.of<NormalizedVisit>({ potentialBot: false, date: '2022-05-05' }),
|
||||||
Mock.of<NormalizedVisit>({ potentialBot: true, date: '2022-05-05' }),
|
Mock.of<NormalizedVisit>({ potentialBot: true, date: '2022-05-05' }),
|
||||||
|
@ -39,12 +29,9 @@ describe('<VisitsTable />', () => {
|
||||||
|
|
||||||
afterEach(jest.resetAllMocks);
|
afterEach(jest.resetAllMocks);
|
||||||
|
|
||||||
it.each([
|
it('renders expected amount of columns', () => {
|
||||||
['2.6.0' as SemVer, 6],
|
setUp([], []);
|
||||||
['2.7.0' as SemVer, 7],
|
expect(screen.getAllByRole('columnheader')).toHaveLength(8);
|
||||||
])('renders expected amount of columns', (version, expectedColumns) => {
|
|
||||||
setUpForServerVersion(version);
|
|
||||||
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedColumns + 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows warning when no visits are found', () => {
|
it('shows warning when no visits are found', () => {
|
||||||
|
@ -104,17 +91,17 @@ describe('<VisitsTable />', () => {
|
||||||
referer: `${index}`,
|
referer: `${index}`,
|
||||||
country: `Country_${index}`,
|
country: `Country_${index}`,
|
||||||
})));
|
})));
|
||||||
const getFirstColumnValue = () => screen.getAllByRole('row')[2]?.querySelectorAll('td')[2]?.textContent;
|
const getFirstColumnValue = () => screen.getAllByRole('row')[2]?.querySelectorAll('td')[3]?.textContent;
|
||||||
const clickColumn = async (index: number) => user.click(screen.getAllByRole('columnheader')[index]);
|
const clickColumn = async (index: number) => user.click(screen.getAllByRole('columnheader')[index]);
|
||||||
|
|
||||||
expect(getFirstColumnValue()).toContain('Country_1');
|
expect(getFirstColumnValue()).toContain('Country_1');
|
||||||
await clickColumn(1); // Date column ASC
|
await clickColumn(2); // Date column ASC
|
||||||
expect(getFirstColumnValue()).toContain('Country_9');
|
expect(getFirstColumnValue()).toContain('Country_9');
|
||||||
await clickColumn(6); // Referer column - ASC
|
await clickColumn(7); // Referer column - ASC
|
||||||
expect(getFirstColumnValue()).toContain('Country_1');
|
expect(getFirstColumnValue()).toContain('Country_1');
|
||||||
await clickColumn(6); // Referer column - DESC
|
await clickColumn(7); // Referer column - DESC
|
||||||
expect(getFirstColumnValue()).toContain('Country_9');
|
expect(getFirstColumnValue()).toContain('Country_9');
|
||||||
await clickColumn(6); // Referer column - reset
|
await clickColumn(7); // Referer column - reset
|
||||||
expect(getFirstColumnValue()).toContain('Country_1');
|
expect(getFirstColumnValue()).toContain('Country_1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -139,12 +126,10 @@ describe('<VisitsTable />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[true, '2.6.0' as SemVer, 8],
|
[true, 9],
|
||||||
[false, '2.6.0' as SemVer, 7],
|
[false, 8],
|
||||||
[true, '2.7.0' as SemVer, 9],
|
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => {
|
||||||
[false, '2.7.0' as SemVer, 8],
|
setUpForOrphanVisits(isOrphanVisits);
|
||||||
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, version, expectedCols) => {
|
|
||||||
setUpForOrphanVisits(isOrphanVisits, version);
|
|
||||||
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols);
|
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,9 @@ import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<VisitsFilterDropdown />', () => {
|
describe('<VisitsFilterDropdown />', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const setUp = (selected: VisitsFilter = {}, isOrphanVisits = true, botsSupported = true) => renderWithEvents(
|
const setUp = (selected: VisitsFilter = {}, isOrphanVisits = true) => renderWithEvents(
|
||||||
<VisitsFilterDropdown
|
<VisitsFilterDropdown
|
||||||
isOrphanVisits={isOrphanVisits}
|
isOrphanVisits={isOrphanVisits}
|
||||||
botsSupported={botsSupported}
|
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>,
|
/>,
|
||||||
|
@ -69,9 +68,4 @@ describe('<VisitsFilterDropdown />', () => {
|
||||||
await user.click(screen.getAllByRole('menuitem')[index]);
|
await user.click(screen.getAllByRole('menuitem')[index]);
|
||||||
expect(onChange).toHaveBeenCalledWith(expectedSelection);
|
expect(onChange).toHaveBeenCalledWith(expectedSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render the component when neither orphan visits or bots filtering will be displayed', () => {
|
|
||||||
const { container } = setUp({}, false, false);
|
|
||||||
expect(container.firstChild).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ describe('VisitsParser', () => {
|
||||||
Mock.of<Visit>({
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||||
referer: 'https://google.com',
|
referer: 'https://google.com',
|
||||||
|
potentialBot: false,
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'Spain',
|
countryName: 'Spain',
|
||||||
cityName: 'Zaragoza',
|
cityName: 'Zaragoza',
|
||||||
|
@ -17,6 +18,7 @@ describe('VisitsParser', () => {
|
||||||
Mock.of<Visit>({
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
||||||
referer: 'https://google.com',
|
referer: 'https://google.com',
|
||||||
|
potentialBot: false,
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'United States',
|
countryName: 'United States',
|
||||||
cityName: 'New York',
|
cityName: 'New York',
|
||||||
|
@ -26,6 +28,7 @@ describe('VisitsParser', () => {
|
||||||
}),
|
}),
|
||||||
Mock.of<Visit>({
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||||
|
potentialBot: false,
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'Spain',
|
countryName: 'Spain',
|
||||||
cityName: '',
|
cityName: '',
|
||||||
|
@ -34,6 +37,7 @@ describe('VisitsParser', () => {
|
||||||
Mock.of<Visit>({
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||||
referer: 'https://m.facebook.com',
|
referer: 'https://m.facebook.com',
|
||||||
|
potentialBot: false,
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'Spain',
|
countryName: 'Spain',
|
||||||
cityName: 'Zaragoza',
|
cityName: 'Zaragoza',
|
||||||
|
@ -52,6 +56,7 @@ describe('VisitsParser', () => {
|
||||||
visitedUrl: 'foo',
|
visitedUrl: 'foo',
|
||||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
||||||
referer: 'https://google.com',
|
referer: 'https://google.com',
|
||||||
|
potentialBot: false,
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'United States',
|
countryName: 'United States',
|
||||||
cityName: 'New York',
|
cityName: 'New York',
|
||||||
|
|
Loading…
Reference in a new issue