mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #441 from acelaya-forks/feature/bots-support
Feature/bots support
This commit is contained in:
commit
a0ab9533cb
19 changed files with 145 additions and 47 deletions
|
@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
* `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided.
|
||||
|
||||
* [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a a `conf.d` folder.
|
||||
* [#440](https://github.com/shlinkio/shlink-web-client/pull/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7.
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
FROM node:14.15-alpine as node
|
||||
FROM node:14.17-alpine as node
|
||||
COPY . /shlink-web-client
|
||||
ARG VERSION="latest"
|
||||
ENV VERSION ${VERSION}
|
||||
RUN cd /shlink-web-client && \
|
||||
npm install && npm run build -- ${VERSION} --no-dist
|
||||
|
||||
FROM nginx:1.19.6-alpine
|
||||
FROM nginx:1.21-alpine
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
|
||||
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY scripts/docker/servers.json_from_env.sh /docker-entrypoint.d/30-shlink-servers-json.sh
|
||||
COPY scripts/docker/servers_from_env.sh /docker-entrypoint.d/30-shlink-servers-json.sh
|
||||
COPY --from=node /shlink-web-client/build /usr/share/nginx/html
|
||||
|
|
|
@ -3,7 +3,7 @@ version: '3'
|
|||
services:
|
||||
shlink_web_client_node:
|
||||
container_name: shlink_web_client_node
|
||||
image: node:14.15-alpine
|
||||
image: node:14.17-alpine
|
||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
|
|
|
@ -23,3 +23,5 @@ export const supportsOrphanVisits = supportsShortUrlTitle;
|
|||
export const supportsQrCodeMargin = supportsShortUrlTitle;
|
||||
|
||||
export const supportsTagsInPatch = supportsShortUrlTitle;
|
||||
|
||||
export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
|
||||
|
|
|
@ -2,17 +2,16 @@ import { RouteComponentProps } from 'react-router';
|
|||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||
import { NormalizedVisit, VisitsInfo } from './types';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
|
||||
export interface OrphanVisitsProps extends RouteComponentProps {
|
||||
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps {
|
||||
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
||||
orphanVisits: VisitsInfo;
|
||||
cancelGetOrphanVisits: () => void;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
|
@ -22,6 +21,7 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
|||
orphanVisits,
|
||||
cancelGetOrphanVisits,
|
||||
settings,
|
||||
selectedServer,
|
||||
}: OrphanVisitsProps) => {
|
||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
||||
|
||||
|
@ -33,6 +33,7 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
|||
baseUrl={url}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
selectedServer={selectedServer}
|
||||
isOrphanVisits
|
||||
>
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
|
|
|
@ -5,20 +5,19 @@ import { ShlinkVisitsParams } from '../api/types';
|
|||
import { parseQuery } from '../utils/helpers/query';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
|
||||
export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: string }> {
|
||||
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> {
|
||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
||||
shortUrlVisits: ShortUrlVisitsState;
|
||||
getShortUrlDetail: Function;
|
||||
shortUrlDetail: ShortUrlDetail;
|
||||
cancelGetShortUrlVisits: () => void;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
|
@ -31,6 +30,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
|
|||
getShortUrlDetail,
|
||||
cancelGetShortUrlVisits,
|
||||
settings,
|
||||
selectedServer,
|
||||
}: ShortUrlVisitsProps) => {
|
||||
const { shortCode } = params;
|
||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||
|
@ -53,6 +53,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
|
|||
domain={domain}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
selectedServer={selectedServer}
|
||||
>
|
||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
|
|
|
@ -3,18 +3,17 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
|||
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||
import TagVisitsHeader from './TagVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
|
||||
export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
||||
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> {
|
||||
getTagVisits: (tag: string, query: any) => void;
|
||||
tagVisits: TagVisitsState;
|
||||
cancelGetTagVisits: () => void;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
|
@ -24,6 +23,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
|||
tagVisits,
|
||||
cancelGetTagVisits,
|
||||
settings,
|
||||
selectedServer,
|
||||
}: TagVisitsProps) => {
|
||||
const { tag } = params;
|
||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
||||
|
@ -37,6 +37,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
|||
baseUrl={url}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
selectedServer={selectedServer}
|
||||
>
|
||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||
</VisitsStats>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/typ
|
|||
import { Result } from '../utils/Result';
|
||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||
import GraphCard from './helpers/GraphCard';
|
||||
import LineChartCard from './helpers/LineChartCard';
|
||||
|
@ -23,13 +24,14 @@ import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, VisitsInfo } f
|
|||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||
import { processStatsFromVisits } from './services/VisitsParser';
|
||||
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
||||
import './VisitsStats.scss';
|
||||
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
|
||||
import './VisitsStats.scss';
|
||||
|
||||
export interface VisitsStatsProps {
|
||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
||||
visitsInfo: VisitsInfo;
|
||||
settings: Settings;
|
||||
selectedServer: SelectedServer;
|
||||
cancelGetVisits: () => void;
|
||||
baseUrl: string;
|
||||
domain?: string;
|
||||
|
@ -67,9 +69,18 @@ const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title
|
|||
</NavLink>
|
||||
);
|
||||
|
||||
const VisitsStats: FC<VisitsStatsProps> = (
|
||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv, isOrphanVisits = false },
|
||||
) => {
|
||||
const VisitsStats: FC<VisitsStatsProps> = ({
|
||||
children,
|
||||
visitsInfo,
|
||||
getVisits,
|
||||
cancelGetVisits,
|
||||
baseUrl,
|
||||
domain,
|
||||
settings,
|
||||
exportCsv,
|
||||
selectedServer,
|
||||
isOrphanVisits = false,
|
||||
}) => {
|
||||
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||
|
@ -243,6 +254,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||
selectedVisits={highlightedVisits}
|
||||
setSelectedVisits={setSelectedVisits}
|
||||
isOrphanVisits={isOrphanVisits}
|
||||
selectedServer={selectedServer}
|
||||
/>
|
||||
</div>
|
||||
</Route>
|
||||
|
|
|
@ -6,24 +6,29 @@ import {
|
|||
faCaretDown as caretDownIcon,
|
||||
faCaretUp as caretUpIcon,
|
||||
faCheck as checkIcon,
|
||||
faRobot as botIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import SimplePaginator from '../common/SimplePaginator';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { determineOrderDir, OrderDir } from '../utils/utils';
|
||||
import { prettify } from '../utils/helpers/numbers';
|
||||
import { supportsBotVisits } from '../utils/helpers/features';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { NormalizedOrphanVisit, NormalizedVisit } from './types';
|
||||
import './VisitsTable.scss';
|
||||
|
||||
interface VisitsTableProps {
|
||||
export interface VisitsTableProps {
|
||||
visits: NormalizedVisit[];
|
||||
selectedVisits?: NormalizedVisit[];
|
||||
setSelectedVisits: (visits: NormalizedVisit[]) => void;
|
||||
matchMedia?: (query: string) => MediaQueryList;
|
||||
isOrphanVisits?: boolean;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl';
|
||||
type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot';
|
||||
|
||||
interface Order {
|
||||
field?: OrderableFields;
|
||||
|
@ -58,6 +63,7 @@ const VisitsTable = ({
|
|||
visits,
|
||||
selectedVisits = [],
|
||||
setSelectedVisits,
|
||||
selectedServer,
|
||||
matchMedia = window.matchMedia,
|
||||
isOrphanVisits = false,
|
||||
}: VisitsTableProps) => {
|
||||
|
@ -69,10 +75,11 @@ const VisitsTable = ({
|
|||
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
|
||||
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
const [ page, setPage ] = useState(1);
|
||||
const end = page * PAGE_SIZE;
|
||||
const start = end - PAGE_SIZE;
|
||||
const supportsBots = supportsBotVisits(selectedServer);
|
||||
const fullSizeColSpan = 7 + Number(supportsBots) + Number(isOrphanVisits);
|
||||
|
||||
const orderByColumn = (field: OrderableFields) =>
|
||||
() => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
||||
|
@ -102,13 +109,19 @@ const VisitsTable = ({
|
|||
<thead className="visits-table__header">
|
||||
<tr>
|
||||
<th
|
||||
className="visits-table__header-cell visits-table__sticky text-center"
|
||||
className={`${headerCellsClass} text-center`}
|
||||
onClick={() => setSelectedVisits(
|
||||
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
|
||||
</th>
|
||||
{supportsBots && (
|
||||
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
|
||||
<FontAwesomeIcon icon={botIcon} />
|
||||
{renderOrderIcon('potentialBot')}
|
||||
</th>
|
||||
)}
|
||||
<th className={headerCellsClass} onClick={orderByColumn('date')}>
|
||||
Date
|
||||
{renderOrderIcon('date')}
|
||||
|
@ -141,7 +154,7 @@ const VisitsTable = ({
|
|||
)}
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="p-0">
|
||||
<td colSpan={fullSizeColSpan} className="p-0">
|
||||
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -149,7 +162,7 @@ const VisitsTable = ({
|
|||
<tbody>
|
||||
{!resultSet.visitsGroups[page - 1]?.length && (
|
||||
<tr>
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="text-center">
|
||||
<td colSpan={fullSizeColSpan} className="text-center">
|
||||
No visits found with current filtering
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -169,6 +182,18 @@ const VisitsTable = ({
|
|||
<td className="text-center">
|
||||
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
||||
</td>
|
||||
{supportsBots && (
|
||||
<td className="text-center">
|
||||
{visit.potentialBot && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
|
||||
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
|
||||
Potentially a visit from a bot or crawler
|
||||
</UncontrolledTooltip>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
|
||||
</td>
|
||||
|
@ -185,7 +210,7 @@ const VisitsTable = ({
|
|||
{resultSet.total > PAGE_SIZE && (
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={isOrphanVisits ? 8 : 7} className="visits-table__footer-cell visits-table__sticky">
|
||||
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky">
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<SimplePaginator
|
||||
|
|
|
@ -81,9 +81,10 @@ export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.redu
|
|||
);
|
||||
|
||||
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
||||
const { userAgent, date, referer, visitLocation } = visit;
|
||||
const { userAgent, date, referer, visitLocation, potentialBot = false } = visit;
|
||||
const common = {
|
||||
date,
|
||||
potentialBot,
|
||||
...parseUserAgent(userAgent),
|
||||
referer: extractDomain(referer),
|
||||
country: visitLocation?.countryName || 'Unknown', // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||
|
|
|
@ -18,19 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
|
||||
bottle.decorator('ShortUrlVisits', connect(
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
|
||||
bottle.decorator('TagVisits', connect(
|
||||
[ 'tagVisits', 'mercureInfo', 'settings' ],
|
||||
[ 'tagVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
|
||||
bottle.decorator('OrphanVisits', connect(
|
||||
[ 'orphanVisits', 'mercureInfo', 'settings' ],
|
||||
[ 'orphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
|
|
7
src/visits/types/CommonVisitsProps.ts
Normal file
7
src/visits/types/CommonVisitsProps.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { SelectedServer } from '../../servers/data';
|
||||
import { Settings } from '../../settings/reducers/settings';
|
||||
|
||||
export interface CommonVisitsProps {
|
||||
selectedServer: SelectedServer;
|
||||
settings: Settings;
|
||||
}
|
|
@ -38,6 +38,7 @@ export interface RegularVisit {
|
|||
date: string;
|
||||
userAgent: string;
|
||||
visitLocation: VisitLocation | null;
|
||||
potentialBot?: boolean; // Optional only when using Shlink older than v2.7
|
||||
}
|
||||
|
||||
export interface OrphanVisit extends RegularVisit {
|
||||
|
@ -59,6 +60,7 @@ export interface NormalizedRegularVisit extends UserAgent {
|
|||
city: string;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
potentialBot: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedOrphanVisit extends NormalizedRegularVisit {
|
||||
|
|
|
@ -9,6 +9,7 @@ import VisitsStats from '../../src/visits/VisitsStats';
|
|||
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
||||
import { Settings } from '../../src/settings/reducers/settings';
|
||||
import { VisitsExporter } from '../../src/visits/services/VisitsExporter';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
|
||||
describe('<OrphanVisits />', () => {
|
||||
it('wraps visits stats and header', () => {
|
||||
|
@ -28,6 +29,7 @@ describe('<OrphanVisits />', () => {
|
|||
location={Mock.all<Location>()}
|
||||
match={Mock.of<match>({ url: 'the_base_url' })}
|
||||
settings={Mock.all<Settings>()}
|
||||
selectedServer={Mock.all<SelectedServer>()}
|
||||
/>,
|
||||
).dive();
|
||||
const stats = wrapper.find(VisitsStats);
|
||||
|
|
|
@ -10,6 +10,7 @@ import LineChartCard from '../../src/visits/helpers/LineChartCard';
|
|||
import VisitsTable from '../../src/visits/VisitsTable';
|
||||
import { Result } from '../../src/utils/Result';
|
||||
import { Settings } from '../../src/settings/reducers/settings';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
|
||||
describe('<VisitStats />', () => {
|
||||
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
||||
|
@ -27,6 +28,7 @@ describe('<VisitStats />', () => {
|
|||
baseUrl={''}
|
||||
settings={Mock.all<Settings>()}
|
||||
exportCsv={exportCsv}
|
||||
selectedServer={Mock.all<SelectedServer>()}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,43 +1,62 @@
|
|||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import VisitsTable from '../../src/visits/VisitsTable';
|
||||
import VisitsTable, { VisitsTableProps } from '../../src/visits/VisitsTable';
|
||||
import { rangeOf } from '../../src/utils/utils';
|
||||
import SimplePaginator from '../../src/common/SimplePaginator';
|
||||
import SearchField from '../../src/utils/SearchField';
|
||||
import { NormalizedVisit } from '../../src/visits/types';
|
||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||
import { SemVer } from '../../src/utils/helpers/version';
|
||||
|
||||
describe('<VisitsTable />', () => {
|
||||
const matchMedia = () => Mock.of<MediaQueryList>({ matches: false });
|
||||
const setSelectedVisits = jest.fn();
|
||||
let wrapper: ShallowWrapper;
|
||||
const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = [], isOrphanVisits = false) => {
|
||||
const wrapperFactory = (props: Partial<VisitsTableProps> = {}) => {
|
||||
wrapper = shallow(
|
||||
<VisitsTable
|
||||
visits={visits}
|
||||
selectedVisits={selectedVisits}
|
||||
setSelectedVisits={setSelectedVisits}
|
||||
visits={[]}
|
||||
selectedServer={Mock.all<SelectedServer>()}
|
||||
{...props}
|
||||
matchMedia={matchMedia}
|
||||
isOrphanVisits={isOrphanVisits}
|
||||
setSelectedVisits={setSelectedVisits}
|
||||
/>,
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => wrapperFactory(
|
||||
{ visits, selectedVisits },
|
||||
);
|
||||
const createOrphanVisitsWrapper = (isOrphanVisits: boolean, version: SemVer) => wrapperFactory({
|
||||
isOrphanVisits,
|
||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
||||
});
|
||||
const createServerVersionWrapper = (version: SemVer) => wrapperFactory({
|
||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: version, version }),
|
||||
});
|
||||
const createWrapperWithBots = () => wrapperFactory({
|
||||
selectedServer: Mock.of<ReachableServer>({ printableVersion: '2.7.0', version: '2.7.0' }),
|
||||
visits: [
|
||||
Mock.of<NormalizedVisit>({ potentialBot: false }),
|
||||
Mock.of<NormalizedVisit>({ potentialBot: true }),
|
||||
],
|
||||
});
|
||||
|
||||
afterEach(jest.resetAllMocks);
|
||||
afterEach(() => wrapper?.unmount());
|
||||
|
||||
it('renders columns as expected', () => {
|
||||
const wrapper = createWrapper([]);
|
||||
it.each([
|
||||
[ '2.6.0' as SemVer, [ 'Date', 'Country', 'City', 'Browser', 'OS', 'Referrer' ]],
|
||||
[ '2.7.0' as SemVer, [ 'fa-robot', 'Date', 'Country', 'City', 'Browser', 'OS', 'Referrer' ]],
|
||||
])('renders columns as expected', (version, expectedColumns) => {
|
||||
const wrapper = createServerVersionWrapper(version);
|
||||
const th = wrapper.find('thead').find('th');
|
||||
|
||||
expect(th).toHaveLength(7);
|
||||
expect(th.at(1).text()).toContain('Date');
|
||||
expect(th.at(2).text()).toContain('Country');
|
||||
expect(th.at(3).text()).toContain('City');
|
||||
expect(th.at(4).text()).toContain('Browser');
|
||||
expect(th.at(5).text()).toContain('OS');
|
||||
expect(th.at(6).text()).toContain('Referrer');
|
||||
expect(th).toHaveLength(expectedColumns.length + 1);
|
||||
expectedColumns.forEach((column, index) => {
|
||||
expect(th.at(index + 1).html()).toContain(column);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows warning when no visits are found', () => {
|
||||
|
@ -137,10 +156,12 @@ describe('<VisitsTable />', () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
[ true, 8 ],
|
||||
[ false, 7 ],
|
||||
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => {
|
||||
const wrapper = createWrapper([], [], isOrphanVisits);
|
||||
[ true, '2.6.0' as SemVer, 8 ],
|
||||
[ false, '2.6.0' as SemVer, 7 ],
|
||||
[ true, '2.7.0' as SemVer, 9 ],
|
||||
[ false, '2.7.0' as SemVer, 8 ],
|
||||
])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, version, expectedCols) => {
|
||||
const wrapper = createOrphanVisitsWrapper(isOrphanVisits, version);
|
||||
const rowsWithColspan = wrapper.find('[colSpan]');
|
||||
const cols = wrapper.find('th');
|
||||
|
||||
|
@ -148,4 +169,12 @@ describe('<VisitsTable />', () => {
|
|||
expect(rowsWithColspan).toHaveLength(2);
|
||||
rowsWithColspan.forEach((row) => expect(row.prop('colSpan')).toEqual(expectedCols));
|
||||
});
|
||||
|
||||
it('displays bots icon when a visit is a potential bot', () => {
|
||||
const wrapper = createWrapperWithBots();
|
||||
const rows = wrapper.find('tbody').find('tr');
|
||||
|
||||
expect(rows.at(0).find('td').at(1).text()).not.toContain('FontAwesomeIcon');
|
||||
expect(rows.at(1).find('td').at(1).text()).toContain('FontAwesomeIcon');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('VisitsExporter', () => {
|
|||
longitude: 0,
|
||||
os: 'os',
|
||||
referer: 'referer',
|
||||
potentialBot: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('VisitsParser', () => {
|
|||
}),
|
||||
Mock.of<Visit>({
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
||||
potentialBot: true,
|
||||
}),
|
||||
];
|
||||
const orphanVisits: OrphanVisit[] = [
|
||||
|
@ -61,6 +62,7 @@ describe('VisitsParser', () => {
|
|||
Mock.of<OrphanVisit>({
|
||||
type: 'regular_404',
|
||||
visitedUrl: 'bar',
|
||||
potentialBot: true,
|
||||
}),
|
||||
Mock.of<OrphanVisit>({
|
||||
type: 'invalid_short_url',
|
||||
|
@ -73,6 +75,7 @@ describe('VisitsParser', () => {
|
|||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
},
|
||||
potentialBot: false,
|
||||
}),
|
||||
];
|
||||
|
||||
|
@ -176,6 +179,7 @@ describe('VisitsParser', () => {
|
|||
date: undefined,
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Firefox',
|
||||
|
@ -186,6 +190,7 @@ describe('VisitsParser', () => {
|
|||
date: undefined,
|
||||
latitude: 1029,
|
||||
longitude: 6758,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
|
@ -196,6 +201,7 @@ describe('VisitsParser', () => {
|
|||
date: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
|
@ -206,6 +212,7 @@ describe('VisitsParser', () => {
|
|||
date: undefined,
|
||||
latitude: 123.45,
|
||||
longitude: -543.21,
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
browser: 'Opera',
|
||||
|
@ -216,6 +223,7 @@ describe('VisitsParser', () => {
|
|||
date: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
potentialBot: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -233,6 +241,7 @@ describe('VisitsParser', () => {
|
|||
longitude: 6758,
|
||||
type: 'base_url',
|
||||
visitedUrl: 'foo',
|
||||
potentialBot: false,
|
||||
},
|
||||
{
|
||||
type: 'regular_404',
|
||||
|
@ -245,6 +254,7 @@ describe('VisitsParser', () => {
|
|||
longitude: undefined,
|
||||
os: 'Others',
|
||||
referer: 'Direct',
|
||||
potentialBot: true,
|
||||
},
|
||||
{
|
||||
browser: 'Chrome',
|
||||
|
@ -257,6 +267,7 @@ describe('VisitsParser', () => {
|
|||
longitude: -543.21,
|
||||
type: 'invalid_short_url',
|
||||
visitedUrl: 'bar',
|
||||
potentialBot: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue