diff --git a/components/input/RefreshButton.js b/components/input/RefreshButton.js index 444f3247..de7ce8cf 100644 --- a/components/input/RefreshButton.js +++ b/components/input/RefreshButton.js @@ -10,11 +10,7 @@ export function RefreshButton({ websiteId, isLoading }) { function handleClick() { if (!isLoading && dateRange) { - if (/^\d+/.test(dateRange.value)) { - setWebsiteDateRange(websiteId, dateRange.value); - } else { - setWebsiteDateRange(websiteId, dateRange); - } + setWebsiteDateRange(websiteId, dateRange); } } diff --git a/components/pages/websites/WebsiteChart.js b/components/pages/websites/WebsiteChart.js index a4d94bd1..fac0fb37 100644 --- a/components/pages/websites/WebsiteChart.js +++ b/components/pages/websites/WebsiteChart.js @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import PageviewsChart from 'components/metrics/PageviewsChart'; import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks'; -import { getDateArray, getDateLength } from 'lib/date'; +import { getDateArray } from 'lib/date'; export function WebsiteChart({ websiteId }) { const [dateRange] = useDateRange(websiteId); @@ -43,7 +43,7 @@ export function WebsiteChart({ websiteId }) { }; } return { pageviews: [], sessions: [] }; - }, [data, startDate, endDate, unit, modified]); + }, [data, startDate, endDate, unit]); return ; } diff --git a/components/pages/websites/WebsiteHeader.js b/components/pages/websites/WebsiteHeader.js index 76f4e4f7..f7db6431 100644 --- a/components/pages/websites/WebsiteHeader.js +++ b/components/pages/websites/WebsiteHeader.js @@ -42,9 +42,9 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) { {name} + - {showLinks && ( {links.map(({ label, icon, path }) => { diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index 17552805..1e1b0616 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -1,20 +1,43 @@ -import { parseDateRange } from 'lib/date'; +import { getMinimumUnit, parseDateRange } from 'lib/date'; import { setItem } from 'next-basics'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; import useLocale from './useLocale'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; +import useApi from './useApi'; export function useDateRange(websiteId) { + const { get } = useApi(); const { locale } = useLocale(); const websiteConfig = websiteStore(state => state[websiteId]?.dateRange); const defaultConfig = DEFAULT_DATE_RANGE; const globalConfig = appStore(state => state.dateRange); const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale); - const saveDateRange = value => { + const saveDateRange = async value => { if (websiteId) { - setWebsiteDateRange(websiteId, value); + let dateRange = value; + + if (typeof value === 'string') { + if (value === 'all') { + const result = await get(`/websites/${websiteId}/daterange`); + const { mindate, maxdate } = result; + + const startDate = new Date(mindate); + const endDate = new Date(maxdate); + + dateRange = { + startDate, + endDate, + unit: getMinimumUnit(startDate, endDate), + value, + }; + } else { + dateRange = parseDateRange(value, locale); + } + } + + setWebsiteDateRange(websiteId, dateRange); } else { setItem(DATE_RANGE_CONFIG, value); setDateRange(value); diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 10722fff..3684d075 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -122,7 +122,7 @@ async function findUnique(data) { throw `${data.length} records found when expecting 1.`; } - return data[0] ?? null; + return findFirst(data); } async function findFirst(data) { diff --git a/lib/query.ts b/lib/query.ts index 09b77df8..88ce62d4 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -7,12 +7,15 @@ export async function parseDateRangeQuery(req: NextApiRequest) { // All-time if (+startAt === 0 && +endAt === 1) { - const { min, max } = await getWebsiteDateRange(websiteId as string); + const result = await getWebsiteDateRange(websiteId as string); + const { min, max } = result[0]; + const startDate = new Date(min); + const endDate = new Date(max); return { - startDate: min, - endDate: max, - unit: getMinimumUnit(min, max), + startDate, + endDate, + unit: getMinimumUnit(startDate, endDate), }; } diff --git a/lib/types.ts b/lib/types.ts index 2e1ed986..7c91ec4f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -137,3 +137,10 @@ export interface RealtimeUpdate { events: any[]; timestamp: number; } + +export interface DateRange { + startDate: Date; + endDate: Date; + unit: string; + value: string; +} diff --git a/pages/api/websites/[id]/daterange.ts b/pages/api/websites/[id]/daterange.ts new file mode 100644 index 00000000..dc043560 --- /dev/null +++ b/pages/api/websites/[id]/daterange.ts @@ -0,0 +1,32 @@ +import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; +import { canViewWebsite } from 'lib/auth'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getWebsiteDateRange } from 'queries'; + +export interface WebsiteDateRangeRequestQuery { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { id: websiteId } = req.query; + + if (req.method === 'GET') { + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const result = await getWebsiteDateRange(websiteId); + + return ok(res, result); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 34347fe5..3e2b96a8 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -73,6 +73,7 @@ export default async ( city, }, }); + const prevPeriod = await getWebsiteStats(websiteId, { startDate: prevStartDate, endDate: prevEndDate, diff --git a/queries/analytics/stats/getWebsiteDateRange.ts b/queries/analytics/stats/getWebsiteDateRange.ts index 1f94c398..45885e45 100644 --- a/queries/analytics/stats/getWebsiteDateRange.ts +++ b/queries/analytics/stats/getWebsiteDateRange.ts @@ -16,32 +16,36 @@ async function relationalQuery(websiteId: string) { const { rawQuery } = prisma; const website = await loadWebsite(websiteId); - return rawQuery( + const result = await rawQuery( ` select - min(created_at) as min, - max(created_at) as max + min(created_at) as mindate, + max(created_at) as maxdate from website_event where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} `, { websiteId, startDate: maxDate(new Date(DEFAULT_RESET_DATE), new Date(website.resetAt)) }, ); + + return result[0] ?? null; } async function clickhouseQuery(websiteId: string) { const { rawQuery } = clickhouse; const website = await loadWebsite(websiteId); - return rawQuery( + const result = await rawQuery( ` select - min(created_at) as min, - max(created_at) as max + min(created_at) as mindate, + max(created_at) as maxdate from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime} `, { websiteId, startDate: maxDate(new Date(DEFAULT_RESET_DATE), new Date(website.resetAt)) }, ); + + return result[0] ?? null; } diff --git a/store/websites.js b/store/websites.ts similarity index 52% rename from store/websites.js rename to store/websites.ts index 34f8242d..0d210af6 100644 --- a/store/websites.js +++ b/store/websites.ts @@ -1,28 +1,20 @@ import { create } from 'zustand'; import produce from 'immer'; -import app from './app'; -import { parseDateRange } from 'lib/date'; +import { DateRange } from 'lib/types'; const store = create(() => ({})); -export function getWebsiteDateRange(websiteId) { +export function getWebsiteDateRange(websiteId: string) { return store.getState()?.[websiteId]; } -export function setWebsiteDateRange(websiteId, value) { +export function setWebsiteDateRange(websiteId: string, dateRange: DateRange) { store.setState( produce(state => { if (!state[websiteId]) { state[websiteId] = {}; } - let dateRange = value; - - if (typeof value === 'string') { - const { locale } = app.getState(); - dateRange = parseDateRange(value, locale); - } - state[websiteId].dateRange = { ...dateRange, modified: Date.now() }; return state;