Skip to content

Commit 30a52ed

Browse files
authored
Merge pull request #848 from acelaya-forks/visits-domain-filter
Allow tag, orphan and non-orphan visits to be filtered by domain
2 parents 9d4a334 + 5fb3655 commit 30a52ed

18 files changed

+170
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
## [Unreleased]
88
### Added
99
* [#839](https://github.com/shlinkio/shlink-web-component/issues/839) Allow filtering short URLs by excluded tags when using Shlink >=4.6.0
10+
* [#838](https://github.com/shlinkio/shlink-web-component/issues/838) Allow filtering tag, orphan and non-orphan visits by domain, when using Shlink >=4.6.0
1011

1112
### Changed
1213
* [#519](https://github.com/shlinkio/shlink-web-component/issues/519) Redesign short URLs filtering bar for more clarity and consistency

src/utils/features.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const supportedFeatures = {
99
advancedQueryRedirectConditions: { minVersion: '4.5.0' },
1010
desktopDeviceTypes: { minVersion: '4.5.0' },
1111
filterShortUrlsByExcludedTags: { minVersion: '4.6.0' },
12+
filterVisitsByDomain: { minVersion: '4.6.0' },
1213
} as const satisfies Record<string, Versions>;
1314

1415
Object.freeze(supportedFeatures);
@@ -27,6 +28,7 @@ const getFeaturesForVersion = (serverVersion: SemVerOrLatest): Record<Feature, b
2728
advancedQueryRedirectConditions: isFeatureEnabledForVersion('advancedQueryRedirectConditions', serverVersion),
2829
desktopDeviceTypes: isFeatureEnabledForVersion('advancedQueryRedirectConditions', serverVersion),
2930
filterShortUrlsByExcludedTags: isFeatureEnabledForVersion('filterShortUrlsByExcludedTags', serverVersion),
31+
filterVisitsByDomain: isFeatureEnabledForVersion('filterVisitsByDomain', serverVersion),
3032
});
3133

3234
const FeaturesContext = createContext(getFeaturesForVersion('0.0.0'));

src/visits/NonOrphanVisits.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
11
import { useCallback } from 'react';
22
import type { FCWithDeps } from '../container/utils';
33
import { componentFactory, useDependencies } from '../container/utils';
4+
import type { DomainsList } from '../domains/reducers/domainsList';
45
import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub';
56
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
67
import { Topics } from '../mercure/helpers/Topics';
78
import type { ReportExporter } from '../utils/services/ReportExporter';
8-
import type { GetVisitsOptions, LoadVisits, VisitsInfo } from './reducers/types';
9+
import type { GetVisitsOptions, LoadWithDomainVisits, VisitsInfo } from './reducers/types';
910
import type { NormalizedVisit, VisitsParams } from './types';
1011
import { VisitsHeader } from './VisitsHeader';
1112
import { VisitsStats } from './VisitsStats';
1213

1314
export type NonOrphanVisitsProps = {
14-
getNonOrphanVisits: (params: LoadVisits) => void;
15+
getNonOrphanVisits: (params: LoadWithDomainVisits) => void;
1516
nonOrphanVisits: VisitsInfo;
1617
cancelGetNonOrphanVisits: () => void;
18+
domainsList: DomainsList;
1719
};
1820

1921
type NonOrphanVisitsDeps = {
2022
ReportExporter: ReportExporter;
2123
};
2224

2325
const NonOrphanVisits: FCWithDeps<MercureBoundProps & NonOrphanVisitsProps, NonOrphanVisitsDeps> = boundToMercureHub((
24-
{ getNonOrphanVisits, nonOrphanVisits, cancelGetNonOrphanVisits },
26+
{ getNonOrphanVisits, nonOrphanVisits, cancelGetNonOrphanVisits, domainsList },
2527
) => {
2628
const { ReportExporter: reportExporter } = useDependencies(NonOrphanVisits);
2729
const exportCsv = useCallback(
2830
(visits: NormalizedVisit[]) => reportExporter.exportVisits('non_orphan_visits.csv', visits),
2931
[reportExporter],
3032
);
3133
const loadVisits = useCallback(
32-
(params: VisitsParams, options: GetVisitsOptions) => getNonOrphanVisits({ options, params }),
34+
(params: VisitsParams, options: GetVisitsOptions) => getNonOrphanVisits({
35+
options,
36+
params,
37+
domain: params.filter?.domain,
38+
}),
3339
[getNonOrphanVisits],
3440
);
3541

@@ -39,6 +45,7 @@ const NonOrphanVisits: FCWithDeps<MercureBoundProps & NonOrphanVisitsProps, NonO
3945
cancelGetVisits={cancelGetNonOrphanVisits}
4046
visitsInfo={nonOrphanVisits}
4147
exportCsv={exportCsv}
48+
domains={domainsList.domains}
4249
>
4350
<VisitsHeader title="Non-orphan visits" visits={nonOrphanVisits.visits} />
4451
</VisitsStats>

src/visits/OrphanVisits.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback, useMemo } from 'react';
22
import type { FCWithDeps } from '../container/utils';
33
import { componentFactory, useDependencies } from '../container/utils';
4+
import type { DomainsList } from '../domains/reducers/domainsList';
45
import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub';
56
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
67
import { Topics } from '../mercure/helpers/Topics';
@@ -18,14 +19,15 @@ export type OrphanVisitsProps = {
1819
orphanVisits: VisitsInfo;
1920
orphanVisitsDeletion: OrphanVisitsDeletion;
2021
cancelGetOrphanVisits: () => void;
22+
domainsList: DomainsList;
2123
};
2224

2325
type OrphanVisitsDeps = {
2426
ReportExporter: ReportExporter
2527
};
2628

2729
const OrphanVisits: FCWithDeps<MercureBoundProps & OrphanVisitsProps, OrphanVisitsDeps> = boundToMercureHub((
28-
{ getOrphanVisits, orphanVisits, cancelGetOrphanVisits, deleteOrphanVisits, orphanVisitsDeletion },
30+
{ getOrphanVisits, orphanVisits, cancelGetOrphanVisits, deleteOrphanVisits, orphanVisitsDeletion, domainsList },
2931
) => {
3032
const { ReportExporter: reportExporter } = useDependencies(OrphanVisits);
3133
const exportCsv = useCallback(
@@ -37,6 +39,7 @@ const OrphanVisits: FCWithDeps<MercureBoundProps & OrphanVisitsProps, OrphanVisi
3739
options,
3840
params,
3941
orphanVisitsType: params.filter?.orphanVisitsType,
42+
domain: params.filter?.domain,
4043
}),
4144
[getOrphanVisits],
4245
);
@@ -53,6 +56,7 @@ const OrphanVisits: FCWithDeps<MercureBoundProps & OrphanVisitsProps, OrphanVisi
5356
exportCsv={exportCsv}
5457
deletion={deletion}
5558
isOrphanVisits
59+
domains={domainsList.domains}
5660
>
5761
<VisitsHeader title="Orphan visits" visits={orphanVisits.visits} />
5862
</VisitsStats>

src/visits/TagVisits.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
22
import { useParams } from 'react-router';
33
import type { FCWithDeps } from '../container/utils';
44
import { componentFactory, useDependencies } from '../container/utils';
5+
import type { DomainsList } from '../domains/reducers/domainsList';
56
import type { MercureBoundProps } from '../mercure/helpers/boundToMercureHub';
67
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
78
import { Topics } from '../mercure/helpers/Topics';
@@ -17,6 +18,7 @@ export type TagVisitsProps = {
1718
getTagVisits: (params: LoadTagVisits) => void;
1819
tagVisits: TagVisitsState;
1920
cancelGetTagVisits: () => void;
21+
domainsList: DomainsList;
2022
};
2123

2224
type TagVisitsDeps = {
@@ -25,12 +27,17 @@ type TagVisitsDeps = {
2527
};
2628

2729
const TagVisits: FCWithDeps<MercureBoundProps & TagVisitsProps, TagVisitsDeps> = boundToMercureHub((
28-
{ getTagVisits, tagVisits, cancelGetTagVisits },
30+
{ getTagVisits, tagVisits, cancelGetTagVisits, domainsList },
2931
) => {
3032
const { ColorGenerator: colorGenerator, ReportExporter: reportExporter } = useDependencies(TagVisits);
3133
const { tag = '' } = useParams();
3234
const loadVisits = useCallback(
33-
(params: VisitsParams, options: GetVisitsOptions) => getTagVisits({ tag, params, options }),
35+
(params: VisitsParams, options: GetVisitsOptions) => getTagVisits({
36+
tag,
37+
params,
38+
options,
39+
domain: params.filter?.domain,
40+
}),
3441
[getTagVisits, tag],
3542
);
3643
const exportCsv = useCallback(
@@ -44,6 +51,7 @@ const TagVisits: FCWithDeps<MercureBoundProps & TagVisitsProps, TagVisitsDeps> =
4451
cancelGetVisits={cancelGetTagVisits}
4552
visitsInfo={tagVisits}
4653
exportCsv={exportCsv}
54+
domains={domainsList.domains}
4755
>
4856
<TagVisitsHeader tagVisits={tagVisits} colorGenerator={colorGenerator} />
4957
</VisitsStats>

src/visits/VisitsStats.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ import { clsx } from 'clsx';
1313
import type { FC, PropsWithChildren } from 'react';
1414
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1515
import { Navigate, Route, Routes, useLocation } from 'react-router';
16+
import type { Domain } from '../domains/data';
17+
import { DomainFilterDropdown } from '../domains/helpers/DomainFilterDropdown';
1618
import { useSetting } from '../settings';
1719
import { ExportBtn } from '../utils/components/ExportBtn';
1820
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
1921
import type { DateInterval, DateRange } from '../utils/dates/helpers/dateIntervals';
2022
import { toDateRange } from '../utils/dates/helpers/dateIntervals';
23+
import { useFeature } from '../utils/features';
2124
import { DoughnutChartCard } from './charts/DoughnutChartCard';
2225
import { LineChartCard } from './charts/LineChartCard';
2326
import { SortableBarChartCard } from './charts/SortableBarChartCard';
@@ -43,6 +46,8 @@ export type VisitsStatsProps = PropsWithChildren<{
4346
};
4447
exportCsv: (visits: NormalizedVisit[]) => void;
4548
isOrphanVisits?: boolean;
49+
/** A domain filter dropdown will be displayed if provided */
50+
domains?: Domain[];
4651
}>;
4752

4853
type VisitsNavLinkOptions = {
@@ -82,9 +87,10 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
8287
deletion,
8388
exportCsv,
8489
isOrphanVisits = false,
90+
domains,
8591
} = props;
8692
const { visits, prevVisits, loading, errorData, fallbackInterval } = visitsInfo;
87-
const [{ dateRange, visitsFilter, loadPrevInterval }, updateQuery] = useVisitsQuery();
93+
const [{ dateRange, visitsFilter, loadPrevInterval, domain }, updateQuery] = useVisitsQuery();
8894
const visitsSettings = useSetting('visits');
8995
const [activeInterval, setActiveInterval] = useState<DateInterval>();
9096
const setDates = useCallback(
@@ -131,7 +137,8 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
131137
...visitsFilter,
132138
excludeBots: visitsFilter.excludeBots ?? visitsSettings?.excludeBots,
133139
loadPrevInterval: loadPrevInterval ?? visitsSettings?.loadPrevInterval,
134-
}), [loadPrevInterval, visitsFilter, visitsSettings?.excludeBots, visitsSettings?.loadPrevInterval]);
140+
domain,
141+
}), [loadPrevInterval, visitsFilter, visitsSettings?.excludeBots, visitsSettings?.loadPrevInterval, domain]);
135142
const mapLocations = useMemo(() => Object.values(citiesForMap), [citiesForMap]);
136143

137144
const selectedBarRef = useRef<string>(undefined);
@@ -153,6 +160,8 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
153160
}
154161
}, [normalizedVisits]);
155162

163+
const filterByDomainIsSupported = useFeature('filterVisitsByDomain');
164+
156165
useEffect(() => cancelGetVisits, [cancelGetVisits]);
157166
useEffect(() => {
158167
const resolvedDateRange = dateRange ?? toDateRange(currentFallbackInterval);
@@ -179,7 +188,7 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
179188
{children}
180189

181190
<section className="flex flex-col lg:flex-row-reverse gap-4">
182-
<div className="lg:flex-3 flex flex-col md:flex-row gap-x-2 gap-y-4">
191+
<div className="lg:w-1/2 flex flex-col md:flex-row gap-x-2 gap-y-4">
183192
<div className="grow">
184193
<DateRangeSelector
185194
disabled={loading}
@@ -188,6 +197,13 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
188197
onDatesChange={setDates}
189198
/>
190199
</div>
200+
{filterByDomainIsSupported && domains && (
201+
<DomainFilterDropdown
202+
domains={loading ? [] : domains}
203+
value={domain}
204+
onChange={(domain) => updateQuery({ domain })}
205+
/>
206+
)}
191207
<VisitsDropdown
192208
disabled={loading}
193209
isOrphanVisits={isOrphanVisits}
@@ -199,7 +215,7 @@ export const VisitsStats: FC<VisitsStatsProps> = (props) => {
199215
})}
200216
/>
201217
</div>
202-
<div className="lg:flex-2 xl:flex-3 flex gap-2">
218+
<div className="lg:w-1/2 xl:flex-3 flex gap-2">
203219
{visits.length > 0 && (
204220
<>
205221
<ExportBtn

src/visits/helpers/hooks.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ type VisitsRawQuery = Record<string, unknown> & {
1717
orphanVisitsType?: ShlinkOrphanVisitType;
1818
excludeBots?: BooleanString;
1919
loadPrevInterval?: BooleanString;
20+
domain?: string;
2021
};
2122

2223
export type VisitsQuery = {
2324
dateRange?: DateRange;
2425
visitsFilter: VisitsFilter;
2526
loadPrevInterval?: boolean;
27+
domain?: string;
2628
};
2729

2830
type UpdateQuery = (extra: DeepPartial<VisitsQuery>) => void;
@@ -44,7 +46,7 @@ const dateFromRangeToQuery = (dateName: keyof DateRange, dateRange?: DateRange):
4446
export const useVisitsQuery = (): [VisitsQuery, UpdateQuery] => {
4547
const navigate = useNavigate();
4648
const rawQuery = useParsedQuery<VisitsRawQuery>();
47-
const { startDate, endDate, orphanVisitsType, excludeBots, loadPrevInterval, ...rest } = rawQuery;
49+
const { startDate, endDate, orphanVisitsType, excludeBots, loadPrevInterval, domain, ...rest } = rawQuery;
4850

4951
const query = useMemo(
5052
(): VisitsQuery => ({
@@ -54,11 +56,15 @@ export const useVisitsQuery = (): [VisitsQuery, UpdateQuery] => {
5456
excludeBots: excludeBots !== undefined ? excludeBots === 'true' : undefined,
5557
},
5658
loadPrevInterval: loadPrevInterval !== undefined ? loadPrevInterval === 'true' : undefined,
59+
domain,
5760
}),
58-
[endDate, excludeBots, loadPrevInterval, orphanVisitsType, startDate],
61+
[endDate, excludeBots, loadPrevInterval, orphanVisitsType, startDate, domain],
5962
);
6063
const updateQuery = useCallback((extra: DeepPartial<VisitsQuery>) => {
61-
const { dateRange, visitsFilter = {}, loadPrevInterval: newLoadPrevInterval } = mergeDeepRight(query, extra);
64+
const { dateRange, visitsFilter = {}, loadPrevInterval: newLoadPrevInterval, domain } = mergeDeepRight(
65+
query,
66+
extra,
67+
);
6268
const { excludeBots: newExcludeBots, orphanVisitsType: newOrphanVisitsType } = visitsFilter;
6369
const newQuery: VisitsRawQuery = {
6470
...rest, // Merge with rest of existing query so that unknown params are preserved
@@ -67,6 +73,7 @@ export const useVisitsQuery = (): [VisitsQuery, UpdateQuery] => {
6773
excludeBots: newExcludeBots === undefined ? undefined : parseBooleanToString(newExcludeBots),
6874
orphanVisitsType: newOrphanVisitsType,
6975
loadPrevInterval: newLoadPrevInterval === undefined ? undefined : parseBooleanToString(newLoadPrevInterval),
76+
domain,
7077
};
7178
const stringifiedQuery = stringifyQueryParams(newQuery);
7279
const queryString = !stringifiedQuery ? '' : `?${stringifiedQuery}`;

src/visits/reducers/nonOrphanVisits.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ShlinkVisitsParams } from '@shlinkio/shlink-js-sdk/api-contract';
22
import type { ShlinkApiClient } from '../../api-contract';
33
import { isBetween } from '../../utils/dates/helpers/date';
44
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
5-
import type { VisitsInfo } from './types';
5+
import type { LoadWithDomainVisits, VisitsInfo } from './types';
66

77
const REDUCER_PREFIX = 'shlink/orphanVisits';
88

@@ -16,11 +16,11 @@ const initialState: VisitsInfo = {
1616

1717
export const getNonOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
1818
typePrefix: `${REDUCER_PREFIX}/getNonOrphanVisits`,
19-
createLoaders: ({ options }) => {
19+
createLoaders: ({ options, domain }: LoadWithDomainVisits) => {
2020
const apiClient = apiClientFactory();
2121
const { doIntervalFallback = false } = options;
2222

23-
const visitsLoader = async (query: ShlinkVisitsParams) => apiClient.getNonOrphanVisits(query);
23+
const visitsLoader = async (query: ShlinkVisitsParams) => apiClient.getNonOrphanVisits({ ...query, domain });
2424
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, (q) => apiClient.getNonOrphanVisits(q));
2525

2626
return { visitsLoader, lastVisitLoader };

src/visits/reducers/orphanVisits.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { isBetween } from '../../utils/dates/helpers/date';
44
import { isOrphanVisit } from '../helpers';
55
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
66
import type { deleteOrphanVisits } from './orphanVisitsDeletion';
7-
import type { LoadVisits, VisitsInfo } from './types';
7+
import type { LoadWithDomainVisits, VisitsInfo } from './types';
88

99
const REDUCER_PREFIX = 'shlink/orphanVisits';
1010

11-
export interface LoadOrphanVisits extends LoadVisits {
11+
export interface LoadOrphanVisits extends LoadWithDomainVisits {
1212
orphanVisitsType?: ShlinkOrphanVisitType;
1313
}
1414

@@ -30,13 +30,14 @@ const filterOrphanVisitsByType = ({ data, ...rest }: ShlinkVisitsList, type?: Sh
3030

3131
export const getOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
3232
typePrefix: `${REDUCER_PREFIX}/getOrphanVisits`,
33-
createLoaders: ({ orphanVisitsType, options }: LoadOrphanVisits) => {
33+
createLoaders: ({ orphanVisitsType, domain, options }: LoadOrphanVisits) => {
3434
const apiClient = apiClientFactory();
3535
const { doIntervalFallback = false } = options;
3636

3737
const visitsLoader = async (query: ShlinkVisitsParams) => apiClient.getOrphanVisits({
3838
...query,
39-
type: orphanVisitsType, // Send type to the server, in case it supports filtering by type
39+
type: orphanVisitsType,
40+
domain,
4041
}).then(
4142
// We still try to filter locally, for Shlink older than 4.0.0
4243
(resp) => filterOrphanVisitsByType(resp, orphanVisitsType),

src/visits/reducers/tagVisits.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ShlinkVisitsParams } from '@shlinkio/shlink-js-sdk/api-contract';
22
import type { ShlinkApiClient } from '../../api-contract';
33
import { filterCreatedVisitsByTag } from '../helpers';
44
import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common';
5-
import type { LoadVisits, VisitsInfo } from './types';
5+
import type { LoadWithDomainVisits, VisitsInfo } from './types';
66

77
const REDUCER_PREFIX = 'shlink/tagVisits';
88

@@ -21,15 +21,15 @@ const initialState: TagVisits = {
2121
progress: null,
2222
};
2323

24-
export type LoadTagVisits = LoadVisits & WithTag;
24+
export type LoadTagVisits = LoadWithDomainVisits & WithTag;
2525

2626
export const getTagVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
2727
typePrefix: `${REDUCER_PREFIX}/getTagVisits`,
28-
createLoaders: ({ tag, options }: LoadTagVisits) => {
28+
createLoaders: ({ tag, options, domain }: LoadTagVisits) => {
2929
const apiClient = apiClientFactory();
3030
const { doIntervalFallback = false } = options;
3131

32-
const visitsLoader = (query: ShlinkVisitsParams) => apiClient.getTagVisits(tag, query);
32+
const visitsLoader = (query: ShlinkVisitsParams) => apiClient.getTagVisits(tag, { ...query, domain });
3333
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (q) => apiClient.getTagVisits(tag, q));
3434

3535
return { visitsLoader, lastVisitLoader };

0 commit comments

Comments
 (0)