From 3a3babadeb56b068cf5d81764e9b8d5272987571 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 09:51:10 +0200 Subject: [PATCH 1/7] Renamed script --- Dockerfile | 2 +- .../docker/{servers.json_from_env.sh => servers_from_env.sh} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/docker/{servers.json_from_env.sh => servers_from_env.sh} (100%) diff --git a/Dockerfile b/Dockerfile index dfe37fd6..78ba67ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,5 +9,5 @@ FROM nginx:1.19.6-alpine LABEL maintainer="Alejandro Celaya " 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 diff --git a/scripts/docker/servers.json_from_env.sh b/scripts/docker/servers_from_env.sh similarity index 100% rename from scripts/docker/servers.json_from_env.sh rename to scripts/docker/servers_from_env.sh From a3550f8e52b2d4c58937a843ee6606dae3dd278d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 09:55:07 +0200 Subject: [PATCH 2/7] Updated docker images --- Dockerfile | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78ba67ca..d495a4eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -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 " 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 diff --git a/docker-compose.yml b/docker-compose.yml index 945494d6..e1e70d5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 From db0c43dcdd610ac80ac6b0c69fcd1241c9c83966 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 11:07:32 +0200 Subject: [PATCH 3/7] Added column to display if a visit is a potential bot in the visits table --- src/utils/helpers/features.ts | 2 ++ src/visits/OrphanVisits.tsx | 7 +++-- src/visits/ShortUrlVisits.tsx | 7 +++-- src/visits/TagVisits.tsx | 7 +++-- src/visits/VisitsStats.tsx | 20 ++++++++++--- src/visits/VisitsTable.tsx | 36 +++++++++++++++++++---- src/visits/services/VisitsParser.ts | 3 +- src/visits/services/provideServices.ts | 6 ++-- src/visits/types/CommonVisitsProps.ts | 7 +++++ src/visits/types/index.ts | 2 ++ test/visits/VisitsTable.test.tsx | 2 ++ test/visits/services/VisitsParser.test.ts | 11 +++++++ 12 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 src/visits/types/CommonVisitsProps.ts diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts index 2caecc75..4d7633df 100644 --- a/src/utils/helpers/features.ts +++ b/src/utils/helpers/features.ts @@ -23,3 +23,5 @@ export const supportsOrphanVisits = supportsShortUrlTitle; export const supportsQrCodeMargin = supportsShortUrlTitle; export const supportsTagsInPatch = supportsShortUrlTitle; + +export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' }); diff --git a/src/visits/OrphanVisits.tsx b/src/visits/OrphanVisits.tsx index b79cfad8..8a252e95 100644 --- a/src/visits/OrphanVisits.tsx +++ b/src/visits/OrphanVisits.tsx @@ -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 > diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index eac8b953..538b4eea 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -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} > diff --git a/src/visits/TagVisits.tsx b/src/visits/TagVisits.tsx index b7dd573b..d4d47eaa 100644 --- a/src/visits/TagVisits.tsx +++ b/src/visits/TagVisits.tsx @@ -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} > diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index ad2e13a1..628964ca 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -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) => void; visitsInfo: VisitsInfo; settings: Settings; + selectedServer: SelectedServer; cancelGetVisits: () => void; baseUrl: string; domain?: string; @@ -67,9 +69,18 @@ const VisitsNavLink: FC = ({ subPath, title ); -const VisitsStats: FC = ( - { children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv, isOrphanVisits = false }, -) => { +const VisitsStats: FC = ({ + children, + visitsInfo, + getVisits, + cancelGetVisits, + baseUrl, + domain, + settings, + exportCsv, + selectedServer, + isOrphanVisits = false, +}) => { const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days'; const [ dateRange, setDateRange ] = useState(intervalToDateRange(initialInterval)); const [ highlightedVisits, setHighlightedVisits ] = useState([]); @@ -243,6 +254,7 @@ const VisitsStats: FC = ( selectedVisits={highlightedVisits} setSelectedVisits={setSelectedVisits} isOrphanVisits={isOrphanVisits} + selectedServer={selectedServer} /> diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index 9eb0eef2..445bf017 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -6,12 +6,16 @@ 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'; @@ -21,9 +25,10 @@ interface VisitsTableProps { 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,12 @@ const VisitsTable = ({ const [ order, setOrder ] = useState({ field: undefined, dir: undefined }); const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]); const isFirstLoad = useRef(true); + const supportsBots = supportsBotVisits(selectedServer); const [ page, setPage ] = useState(1); const end = page * PAGE_SIZE; const start = end - PAGE_SIZE; + const fullSizeColSpan = 7 + Number(supportsBots) + Number(isOrphanVisits); const orderByColumn = (field: OrderableFields) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) }); @@ -102,13 +110,19 @@ const VisitsTable = ({ setSelectedVisits( selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [], )} > 0 })} /> + {supportsBots && ( + + + {renderOrderIcon('potentialBot')} + + )} Date {renderOrderIcon('date')} @@ -141,7 +155,7 @@ const VisitsTable = ({ )} - + @@ -149,7 +163,7 @@ const VisitsTable = ({ {!resultSet.visitsGroups[page - 1]?.length && ( - + No visits found with current filtering @@ -169,6 +183,18 @@ const VisitsTable = ({ {isSelected && } + {supportsBots && ( + + {visit.potentialBot && ( + <> + + + Potentially a visit from a bot or crawler + + + )} + + )} {visit.date} @@ -185,7 +211,7 @@ const VisitsTable = ({ {resultSet.total > PAGE_SIZE && ( - +
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 diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index 733cac95..9eb3f5eb 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -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' ], )); diff --git a/src/visits/types/CommonVisitsProps.ts b/src/visits/types/CommonVisitsProps.ts new file mode 100644 index 00000000..7e5a21b4 --- /dev/null +++ b/src/visits/types/CommonVisitsProps.ts @@ -0,0 +1,7 @@ +import { SelectedServer } from '../../servers/data'; +import { Settings } from '../../settings/reducers/settings'; + +export interface CommonVisitsProps { + selectedServer: SelectedServer; + settings: Settings; +} diff --git a/src/visits/types/index.ts b/src/visits/types/index.ts index 0e2879d5..ffaccb47 100644 --- a/src/visits/types/index.ts +++ b/src/visits/types/index.ts @@ -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 { diff --git a/test/visits/VisitsTable.test.tsx b/test/visits/VisitsTable.test.tsx index 393ab64d..65c71992 100644 --- a/test/visits/VisitsTable.test.tsx +++ b/test/visits/VisitsTable.test.tsx @@ -5,6 +5,7 @@ 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 { SelectedServer } from '../../src/servers/data'; describe('', () => { const matchMedia = () => Mock.of({ matches: false }); @@ -18,6 +19,7 @@ describe('', () => { setSelectedVisits={setSelectedVisits} matchMedia={matchMedia} isOrphanVisits={isOrphanVisits} + selectedServer={Mock.all()} />, ); diff --git a/test/visits/services/VisitsParser.test.ts b/test/visits/services/VisitsParser.test.ts index 7edafab8..d629985e 100644 --- a/test/visits/services/VisitsParser.test.ts +++ b/test/visits/services/VisitsParser.test.ts @@ -43,6 +43,7 @@ describe('VisitsParser', () => { }), Mock.of({ 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({ type: 'regular_404', visitedUrl: 'bar', + potentialBot: true, }), Mock.of({ 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, }, ]); }); From a30376344e7ee179d16525286b9daf9e7af63839 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 11:38:13 +0200 Subject: [PATCH 4/7] Added tests covering visits table with potential bots --- src/visits/VisitsTable.tsx | 5 ++-- test/visits/VisitsTable.test.tsx | 43 +++++++++++++++++++------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index 445bf017..3c569ddc 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -19,7 +19,7 @@ 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; @@ -75,11 +75,10 @@ const VisitsTable = ({ const [ order, setOrder ] = useState({ field: undefined, dir: undefined }); const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]); const isFirstLoad = useRef(true); - const supportsBots = supportsBotVisits(selectedServer); - 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) => diff --git a/test/visits/VisitsTable.test.tsx b/test/visits/VisitsTable.test.tsx index 65c71992..6972cf35 100644 --- a/test/visits/VisitsTable.test.tsx +++ b/test/visits/VisitsTable.test.tsx @@ -1,45 +1,52 @@ 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 { SelectedServer } from '../../src/servers/data'; +import { ReachableServer, SelectedServer } from '../../src/servers/data'; +import { SemVer } from '../../src/utils/helpers/version'; describe('', () => { const matchMedia = () => Mock.of({ matches: false }); const setSelectedVisits = jest.fn(); let wrapper: ShallowWrapper; - const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = [], isOrphanVisits = false) => { + const wrapperFactory = (props: Partial = {}) => { wrapper = shallow( ()} + {...props} + matchMedia={matchMedia} + setSelectedVisits={setSelectedVisits} />, ); return wrapper; }; + const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => wrapperFactory( + { visits, selectedVisits }, + ); + const createOrphanVisitsWrapper = (isOrphanVisits: boolean) => wrapperFactory({ isOrphanVisits }); + const createServerVersionWrapper = (version: SemVer) => wrapperFactory({ + selectedServer: Mock.of({ printableVersion: version, version }), + }); 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', () => { @@ -142,7 +149,7 @@ describe('', () => { [ true, 8 ], [ false, 7 ], ])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => { - const wrapper = createWrapper([], [], isOrphanVisits); + const wrapper = createOrphanVisitsWrapper(isOrphanVisits); const rowsWithColspan = wrapper.find('[colSpan]'); const cols = wrapper.find('th'); From 151175dc708d7efdadb2860b32adc0db208eff1b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 11:41:41 +0200 Subject: [PATCH 5/7] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b7ea02..a096c278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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* From 1cf96c72124f4da99ab6616c973dc39eaa71d6d4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 11:49:53 +0200 Subject: [PATCH 6/7] Improved VisitsTable test --- test/visits/VisitsTable.test.tsx | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/test/visits/VisitsTable.test.tsx b/test/visits/VisitsTable.test.tsx index 6972cf35..7abc95a0 100644 --- a/test/visits/VisitsTable.test.tsx +++ b/test/visits/VisitsTable.test.tsx @@ -28,10 +28,20 @@ describe('', () => { const createWrapper = (visits: NormalizedVisit[], selectedVisits: NormalizedVisit[] = []) => wrapperFactory( { visits, selectedVisits }, ); - const createOrphanVisitsWrapper = (isOrphanVisits: boolean) => wrapperFactory({ isOrphanVisits }); + const createOrphanVisitsWrapper = (isOrphanVisits: boolean, version: SemVer) => wrapperFactory({ + isOrphanVisits, + selectedServer: Mock.of({ printableVersion: version, version }), + }); const createServerVersionWrapper = (version: SemVer) => wrapperFactory({ selectedServer: Mock.of({ printableVersion: version, version }), }); + const createWrapperWithBots = () => wrapperFactory({ + selectedServer: Mock.of({ printableVersion: '2.7.0', version: '2.7.0' }), + visits: [ + Mock.of({ potentialBot: false }), + Mock.of({ potentialBot: true }), + ], + }); afterEach(jest.resetAllMocks); afterEach(() => wrapper?.unmount()); @@ -146,10 +156,12 @@ describe('', () => { }); it.each([ - [ true, 8 ], - [ false, 7 ], - ])('displays proper amount of columns for orphan and non-orphan visits', (isOrphanVisits, expectedCols) => { - const wrapper = createOrphanVisitsWrapper(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'); @@ -157,4 +169,12 @@ describe('', () => { 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'); + }); }); From 7b80948eeae4aa215d5d171434dc9b51744a025b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 13 Jun 2021 11:54:51 +0200 Subject: [PATCH 7/7] Fixed TS errors in tests --- test/visits/OrphanVisits.test.tsx | 2 ++ test/visits/VisitsStats.test.tsx | 2 ++ test/visits/services/VisitsExporter.test.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/test/visits/OrphanVisits.test.tsx b/test/visits/OrphanVisits.test.tsx index 020d54f9..e980c4fa 100644 --- a/test/visits/OrphanVisits.test.tsx +++ b/test/visits/OrphanVisits.test.tsx @@ -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('', () => { it('wraps visits stats and header', () => { @@ -28,6 +29,7 @@ describe('', () => { location={Mock.all()} match={Mock.of({ url: 'the_base_url' })} settings={Mock.all()} + selectedServer={Mock.all()} />, ).dive(); const stats = wrapper.find(VisitsStats); diff --git a/test/visits/VisitsStats.test.tsx b/test/visits/VisitsStats.test.tsx index 2edb4fbe..c67bff66 100644 --- a/test/visits/VisitsStats.test.tsx +++ b/test/visits/VisitsStats.test.tsx @@ -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('', () => { const visits = [ Mock.all(), Mock.all(), Mock.all() ]; @@ -27,6 +28,7 @@ describe('', () => { baseUrl={''} settings={Mock.all()} exportCsv={exportCsv} + selectedServer={Mock.all()} />, ); diff --git a/test/visits/services/VisitsExporter.test.ts b/test/visits/services/VisitsExporter.test.ts index e21c923a..da91fd78 100644 --- a/test/visits/services/VisitsExporter.test.ts +++ b/test/visits/services/VisitsExporter.test.ts @@ -39,6 +39,7 @@ describe('VisitsExporter', () => { longitude: 0, os: 'os', referer: 'referer', + potentialBot: false, }, ];