mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #758 from acelaya-forks/feature/fix-visits-interval
Feature/fix visits interval
This commit is contained in:
commit
261cc68624
14 changed files with 68 additions and 53 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [3.8.1] - 2022-12-06
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#756](https://github.com/shlinkio/shlink-web-client/issues/756) Fixed all visits interval not working unless switching to a different interval first.
|
||||||
|
* [#757](https://github.com/shlinkio/shlink-web-client/issues/757) Fixed visits fallback interval not working until the visits view has been loaded at least twice.
|
||||||
|
|
||||||
|
|
||||||
## [3.8.0] - 2022-12-03
|
## [3.8.0] - 2022-12-03
|
||||||
### Added
|
### Added
|
||||||
* [#708](https://github.com/shlinkio/shlink-web-client/issues/708) Added support for API v3.
|
* [#708](https://github.com/shlinkio/shlink-web-client/issues/708) Added support for API v3.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
intervalToDateRange,
|
intervalToDateRange,
|
||||||
rangeIsInterval,
|
rangeIsInterval,
|
||||||
dateRangeIsEmpty,
|
dateRangeIsEmpty,
|
||||||
|
ALL,
|
||||||
} from '../helpers/dateIntervals';
|
} from '../helpers/dateIntervals';
|
||||||
import { DateRangeRow } from './DateRangeRow';
|
import { DateRangeRow } from './DateRangeRow';
|
||||||
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
|
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
|
||||||
|
@ -31,7 +32,7 @@ export const DateRangeSelector = (
|
||||||
const [activeDateRange, setActiveDateRange] = useState(initialIntervalIsRange ? undefined : initialDateRange);
|
const [activeDateRange, setActiveDateRange] = useState(initialIntervalIsRange ? undefined : initialDateRange);
|
||||||
|
|
||||||
const updateDateRange = (dateRange: DateRange) => {
|
const updateDateRange = (dateRange: DateRange) => {
|
||||||
setActiveInterval(dateRangeIsEmpty(dateRange) ? 'all' : undefined);
|
setActiveInterval(dateRangeIsEmpty(dateRange) ? ALL : undefined);
|
||||||
setActiveDateRange(dateRange);
|
setActiveDateRange(dateRange);
|
||||||
onDatesChange(dateRange);
|
onDatesChange(dateRange);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
|
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
|
||||||
import { isDateObject, STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date';
|
import { isDateObject, now, STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date';
|
||||||
|
|
||||||
export interface TimeProps {
|
export interface TimeProps {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
@ -12,7 +12,7 @@ export const Time = ({ date, format = STANDARD_DATE_AND_TIME_FORMAT, relative =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time dateTime={`${getUnixTime(dateObject)}000`}>
|
<time dateTime={`${getUnixTime(dateObject)}000`}>
|
||||||
{relative ? `${formatDistance(new Date(), dateObject)} ago` : formatDate(dateObject, format)}
|
{relative ? `${formatDistance(now(), dateObject)} ago` : formatDate(dateObject, format)}
|
||||||
</time>
|
</time>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,8 @@ export type DateOrString = Date | string;
|
||||||
|
|
||||||
type NullableDate = DateOrString | null;
|
type NullableDate = DateOrString | null;
|
||||||
|
|
||||||
|
export const now = () => new Date();
|
||||||
|
|
||||||
export const isDateObject = (date: DateOrString): date is Date => typeof date !== 'string';
|
export const isDateObject = (date: DateOrString): date is Date => typeof date !== 'string';
|
||||||
|
|
||||||
const formatDateFromFormat = (date?: NullableDate, theFormat?: string): OptionalString => {
|
const formatDateFromFormat = (date?: NullableDate, theFormat?: string): OptionalString => {
|
||||||
|
@ -28,7 +30,7 @@ export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date,
|
||||||
|
|
||||||
export const formatInternational = formatDate();
|
export const formatInternational = formatDate();
|
||||||
|
|
||||||
export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, new Date());
|
export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, now());
|
||||||
|
|
||||||
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
|
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { subDays, startOfDay, endOfDay } from 'date-fns';
|
import { subDays, startOfDay, endOfDay } from 'date-fns';
|
||||||
import { cond, filter, isEmpty, T } from 'ramda';
|
import { cond, filter, isEmpty, T } from 'ramda';
|
||||||
import { dateOrNull, DateOrString, formatInternational, isBeforeOrEqual, parseISO } from './date';
|
import { dateOrNull, DateOrString, formatInternational, isBeforeOrEqual, now, parseISO } from './date';
|
||||||
|
import { equals } from '../utils';
|
||||||
|
|
||||||
export interface DateRange {
|
export interface DateRange {
|
||||||
startDate?: Date | null;
|
startDate?: Date | null;
|
||||||
endDate?: Date | null;
|
endDate?: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL = 'all';
|
export const ALL = 'all';
|
||||||
const INTERVAL_TO_STRING_MAP = {
|
const INTERVAL_TO_STRING_MAP = {
|
||||||
today: 'Today',
|
today: 'Today',
|
||||||
yesterday: 'Yesterday',
|
yesterday: 'Yesterday',
|
||||||
|
@ -64,39 +65,25 @@ export const rangeOrIntervalToString = (range?: DateRange | DateInterval): strin
|
||||||
return INTERVAL_TO_STRING_MAP[range];
|
return INTERVAL_TO_STRING_MAP[range];
|
||||||
};
|
};
|
||||||
|
|
||||||
const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(new Date(), daysAgo));
|
const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(now(), daysAgo));
|
||||||
const endingToday = (startDate: Date): DateRange => ({ startDate, endDate: endOfDay(new Date()) });
|
const endingToday = (startDate: Date): DateRange => ({ startDate, endDate: endOfDay(now()) });
|
||||||
|
|
||||||
export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
|
export const intervalToDateRange = cond<[DateInterval | undefined], DateRange>([
|
||||||
if (!dateInterval || dateInterval === ALL) {
|
[equals('today'), () => endingToday(startOfDay(now()))],
|
||||||
return {};
|
[equals('yesterday'), () => ({ startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(now(), 1)) })],
|
||||||
}
|
[equals('last7Days'), () => endingToday(startOfDaysAgo(7))],
|
||||||
|
[equals('last30Days'), () => endingToday(startOfDaysAgo(30))],
|
||||||
switch (dateInterval) {
|
[equals('last90Days'), () => endingToday(startOfDaysAgo(90))],
|
||||||
case 'today':
|
[equals('last180Days'), () => endingToday(startOfDaysAgo(180))],
|
||||||
return endingToday(startOfDay(new Date()));
|
[equals('last365Days'), () => endingToday(startOfDaysAgo(365))],
|
||||||
case 'yesterday':
|
[T, () => ({})],
|
||||||
return { startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(new Date(), 1)) };
|
]);
|
||||||
case 'last7Days':
|
|
||||||
return endingToday(startOfDaysAgo(7));
|
|
||||||
case 'last30Days':
|
|
||||||
return endingToday(startOfDaysAgo(30));
|
|
||||||
case 'last90Days':
|
|
||||||
return endingToday(startOfDaysAgo(90));
|
|
||||||
case 'last180Days':
|
|
||||||
return endingToday(startOfDaysAgo(180));
|
|
||||||
case 'last365Days':
|
|
||||||
return endingToday(startOfDaysAgo(365));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dateToMatchingInterval = (date: DateOrString): DateInterval => {
|
export const dateToMatchingInterval = (date: DateOrString): DateInterval => {
|
||||||
const theDate: Date = parseISO(date);
|
const theDate = parseISO(date);
|
||||||
|
|
||||||
return cond<never, DateInterval>([
|
return cond<never, DateInterval>([
|
||||||
[() => isBeforeOrEqual(startOfDay(new Date()), theDate), () => 'today'],
|
[() => isBeforeOrEqual(startOfDay(now()), theDate), () => 'today'],
|
||||||
[() => isBeforeOrEqual(startOfDaysAgo(1), theDate), () => 'yesterday'],
|
[() => isBeforeOrEqual(startOfDaysAgo(1), theDate), () => 'yesterday'],
|
||||||
[() => isBeforeOrEqual(startOfDaysAgo(7), theDate), () => 'last7Days'],
|
[() => isBeforeOrEqual(startOfDaysAgo(7), theDate), () => 'last7Days'],
|
||||||
[() => isBeforeOrEqual(startOfDaysAgo(30), theDate), () => 'last30Days'],
|
[() => isBeforeOrEqual(startOfDaysAgo(30), theDate), () => 'last30Days'],
|
||||||
|
|
|
@ -24,3 +24,5 @@ export type OptionalString = Optional<string>;
|
||||||
export const nonEmptyValueOrNull = <T>(value: T): T | null => (isEmpty(value) ? null : value);
|
export const nonEmptyValueOrNull = <T>(value: T): T | null => (isEmpty(value) ? null : value);
|
||||||
|
|
||||||
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
export const capitalize = <T extends string>(value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
||||||
|
|
||||||
|
export const equals = (value: any) => (otherValue: any) => value === otherValue;
|
||||||
|
|
|
@ -122,6 +122,12 @@ export const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
getVisits({ dateRange: resolvedDateRange, filter: visitsFilter }, isFirstLoad.current);
|
getVisits({ dateRange: resolvedDateRange, filter: visitsFilter }, isFirstLoad.current);
|
||||||
isFirstLoad.current = false;
|
isFirstLoad.current = false;
|
||||||
}, [dateRange, visitsFilter]);
|
}, [dateRange, visitsFilter]);
|
||||||
|
useEffect(() => {
|
||||||
|
// As soon as the fallback is loaded, if the initial interval used the settings one, we do fall back
|
||||||
|
if (fallbackInterval && initialInterval.current === (settings.visits?.defaultInterval ?? 'last30Days')) {
|
||||||
|
initialInterval.current = fallbackInterval;
|
||||||
|
}
|
||||||
|
}, [fallbackInterval]);
|
||||||
|
|
||||||
const renderVisitsContent = () => {
|
const renderVisitsContent = () => {
|
||||||
if (loadingLarge) {
|
if (loadingLarge) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const useVisitsQuery = (): [VisitsFiltering, UpdateFiltering] => {
|
||||||
({ startDate, endDate, orphanVisitsType, excludeBots, domain }: VisitsQuery): VisitsFilteringAndDomain => ({
|
({ startDate, endDate, orphanVisitsType, excludeBots, domain }: VisitsQuery): VisitsFilteringAndDomain => ({
|
||||||
domain,
|
domain,
|
||||||
filtering: {
|
filtering: {
|
||||||
dateRange: startDate || endDate ? datesToDateRange(startDate, endDate) : undefined,
|
dateRange: startDate != null || endDate != null ? datesToDateRange(startDate, endDate) : undefined,
|
||||||
visitsFilter: { orphanVisitsType, excludeBots: excludeBots === 'true' },
|
visitsFilter: { orphanVisitsType, excludeBots: excludeBots === 'true' },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -47,8 +47,8 @@ export const useVisitsQuery = (): [VisitsFiltering, UpdateFiltering] => {
|
||||||
const updateFiltering = (extra: DeepPartial<VisitsFiltering>) => {
|
const updateFiltering = (extra: DeepPartial<VisitsFiltering>) => {
|
||||||
const { dateRange, visitsFilter } = mergeDeepRight(filtering, extra);
|
const { dateRange, visitsFilter } = mergeDeepRight(filtering, extra);
|
||||||
const query: VisitsQuery = {
|
const query: VisitsQuery = {
|
||||||
startDate: (dateRange?.startDate && formatIsoDate(dateRange.startDate)) || undefined,
|
startDate: (dateRange?.startDate && formatIsoDate(dateRange.startDate)) || '',
|
||||||
endDate: (dateRange?.endDate && formatIsoDate(dateRange.endDate)) || undefined,
|
endDate: (dateRange?.endDate && formatIsoDate(dateRange.endDate)) || '',
|
||||||
excludeBots: visitsFilter.excludeBots ? 'true' : undefined,
|
excludeBots: visitsFilter.excludeBots ? 'true' : undefined,
|
||||||
orphanVisitsType: visitsFilter.orphanVisitsType,
|
orphanVisitsType: visitsFilter.orphanVisitsType,
|
||||||
domain: theDomain,
|
domain: theDomain,
|
||||||
|
|
|
@ -8,11 +8,10 @@ import {
|
||||||
rangeOrIntervalToString,
|
rangeOrIntervalToString,
|
||||||
toDateRange,
|
toDateRange,
|
||||||
} from '../../../src/utils/helpers/dateIntervals';
|
} from '../../../src/utils/helpers/dateIntervals';
|
||||||
import { parseDate } from '../../../src/utils/helpers/date';
|
import { parseDate, now } from '../../../src/utils/helpers/date';
|
||||||
|
|
||||||
describe('date-types', () => {
|
describe('date-types', () => {
|
||||||
const now = () => new Date();
|
const daysBack = (days: number) => subDays(now(), days);
|
||||||
const daysBack = (days: number) => subDays(new Date(), days);
|
|
||||||
|
|
||||||
describe('dateRangeIsEmpty', () => {
|
describe('dateRangeIsEmpty', () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
@ -26,9 +25,9 @@ describe('date-types', () => {
|
||||||
[{ startDate: undefined, endDate: undefined }, true],
|
[{ startDate: undefined, endDate: undefined }, true],
|
||||||
[{ startDate: undefined, endDate: null }, true],
|
[{ startDate: undefined, endDate: null }, true],
|
||||||
[{ startDate: null, endDate: undefined }, true],
|
[{ startDate: null, endDate: undefined }, true],
|
||||||
[{ startDate: new Date() }, false],
|
[{ startDate: now() }, false],
|
||||||
[{ endDate: new Date() }, false],
|
[{ endDate: now() }, false],
|
||||||
[{ startDate: new Date(), endDate: new Date() }, false],
|
[{ startDate: now(), endDate: now() }, false],
|
||||||
])('returns proper result', (dateRange, expectedResult) => {
|
])('returns proper result', (dateRange, expectedResult) => {
|
||||||
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
|
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
|
@ -197,12 +197,12 @@ describe('domainVisitsReducer', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 20)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 100)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
|
|
|
@ -177,12 +177,12 @@ describe('nonOrphanVisitsReducer', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 5)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 200)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
|
|
|
@ -175,12 +175,12 @@ describe('orphanVisitsReducer', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 5)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 200)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
|
|
|
@ -219,12 +219,12 @@ describe('shortUrlVisitsReducer', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 5)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 5)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 200)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 200)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
|
|
|
@ -193,12 +193,12 @@ describe('tagVisitsReducer', () => {
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 20)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 20)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 100)) })],
|
[Mock.of<Visit>({ date: formatISO(subDays(now, 100)) })],
|
||||||
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
|
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
|
||||||
3,
|
3,
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue