Skip to content

Commit 8fa9937

Browse files
committed
Add domains filter control to tags, orphan and non-orphan visits
1 parent 43cb35f commit 8fa9937

8 files changed

Lines changed: 48 additions & 13 deletions

File tree

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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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';
@@ -14,14 +15,15 @@ export type NonOrphanVisitsProps = {
1415
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(
@@ -43,6 +45,7 @@ const NonOrphanVisits: FCWithDeps<MercureBoundProps & NonOrphanVisitsProps, NonO
4345
cancelGetVisits={cancelGetNonOrphanVisits}
4446
visitsInfo={nonOrphanVisits}
4547
exportCsv={exportCsv}
48+
domains={domainsList.domains}
4649
>
4750
<VisitsHeader title="Non-orphan visits" visits={nonOrphanVisits.visits} />
4851
</VisitsStats>

src/visits/OrphanVisits.tsx

Lines changed: 4 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(
@@ -54,6 +56,7 @@ const OrphanVisits: FCWithDeps<MercureBoundProps & OrphanVisitsProps, OrphanVisi
5456
exportCsv={exportCsv}
5557
deletion={deletion}
5658
isOrphanVisits
59+
domains={domainsList.domains}
5760
>
5861
<VisitsHeader title="Orphan visits" visits={orphanVisits.visits} />
5962
</VisitsStats>

src/visits/TagVisits.tsx

Lines changed: 4 additions & 1 deletion
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,7 +27,7 @@ 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();
@@ -49,6 +51,7 @@ const TagVisits: FCWithDeps<MercureBoundProps & TagVisitsProps, TagVisitsDeps> =
4951
cancelGetVisits={cancelGetTagVisits}
5052
visitsInfo={tagVisits}
5153
exportCsv={exportCsv}
54+
domains={domainsList.domains}
5255
>
5356
<TagVisitsHeader tagVisits={tagVisits} colorGenerator={colorGenerator} />
5457
</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/services/provideServices.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
5757
]));
5858

5959
bottle.factory('TagVisits', TagVisitsFactory);
60-
bottle.decorator('TagVisits', connectWithMercure(['tagVisits'], ['getTagVisits', 'cancelGetTagVisits']));
60+
bottle.decorator('TagVisits', connectWithMercure(['tagVisits', 'domainsList'], ['getTagVisits', 'cancelGetTagVisits']));
6161

6262
bottle.factory('TagVisitsComparison', TagVisitsComparisonFactory);
6363
bottle.decorator('TagVisitsComparison', connectWithMercure(
@@ -86,13 +86,13 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
8686

8787
bottle.factory('OrphanVisits', OrphanVisitsFactory);
8888
bottle.decorator('OrphanVisits', connectWithMercure(
89-
['orphanVisits', 'orphanVisitsDeletion'],
89+
['orphanVisits', 'orphanVisitsDeletion', 'domainsList'],
9090
['getOrphanVisits', 'cancelGetOrphanVisits', 'deleteOrphanVisits'],
9191
));
9292

9393
bottle.factory('NonOrphanVisits', NonOrphanVisitsFactory);
9494
bottle.decorator('NonOrphanVisits', connectWithMercure(
95-
['nonOrphanVisits'],
95+
['nonOrphanVisits', 'domainsList'],
9696
['getNonOrphanVisits', 'cancelGetNonOrphanVisits'],
9797
));
9898

0 commit comments

Comments
 (0)